Coverage Report - org.apache.maven.plugin.changelog.ChangeLogReport
 
Classes in this File Line Coverage Branch Coverage Complexity
ChangeLogReport
59%
295/501
41%
98/240
4,538
ChangeLogReport$1
100%
4/4
N/A
4,538
 
 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  17
 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  17
     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  17
         if ( !basedir.exists() )
 419  
         {
 420  3
             doGenerateEmptyReport( getBundle( locale ), getSink() );
 421  
 
 422  3
             return;
 423  
         }
 424  
 
 425  14
         initializeDefaultConfigurationParameters();
 426  
 
 427  14
         initializeDeveloperMaps();
 428  
 
 429  14
         verifySCMTypeParams();
 430  
 
 431  13
         if ( systemProperties != null )
 432  
         {
 433  
             // Add all system properties configured by the user
 434  0
             Iterator iter = systemProperties.keySet().iterator();
 435  
 
 436  0
             while ( iter.hasNext() )
 437  
             {
 438  0
                 String key = (String) iter.next();
 439  
 
 440  0
                 String value = systemProperties.getProperty( key );
 441  
 
 442  0
                 System.setProperty( key, value );
 443  
 
 444  0
                 getLog().debug( "Setting system property: " + key + "=" + value );
 445  0
             }
 446  
         }
 447  
 
 448  13
         doGenerateReport( getChangedSets(), getBundle( locale ), getSink() );
 449  10
     }
 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  14
         if ( displayFileRevDetailUrl == null || displayFileRevDetailUrl.length() == 0 )
 458  
         {
 459  14
             displayFileRevDetailUrl = displayFileDetailUrl;
 460  
         }
 461  14
     }
 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  14
         developersById = new HashMap();
 470  14
         developersByName = new HashMap();
 471  
 
 472  14
         if ( developers != null )
 473  
         {
 474  1
             for ( Iterator i = developers.iterator(); i.hasNext(); )
 475  
             {
 476  2
                 Developer developer = (Developer) i.next();
 477  
 
 478  2
                 developersById.put( developer.getId(), developer );
 479  2
                 developersByName.put( developer.getName(), developer );
 480  2
             }
 481  
         }
 482  14
     }
 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  13
         List changelogList = null;
 494  
 
 495  13
         if ( !outputXML.isAbsolute() )
 496  
         {
 497  10
             outputXML = new File( project.getBasedir(), outputXML.getPath() );
 498  
         }
 499  
 
 500  13
         if ( outputXML.exists() )
 501  
         {
 502  10
             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  10
                     getLog().info( "Using existing changelog.xml..." );
 511  
 
 512  10
                     changelogList = ChangeLog.loadChangedSets( ReaderFactory.newReader( outputXML, outputEncoding ) );
 513  
                 }
 514  0
                 catch ( FileNotFoundException e )
 515  
                 {
 516  
                     //do nothing, just regenerate
 517  
                 }
 518  0
                 catch ( Exception e )
 519  
                 {
 520  0
                     throw new MavenReportException( "An error occurred while parsing " + outputXML.getAbsolutePath(),
 521  
                                                     e );
 522  10
                 }
 523  
             }
 524  
         }
 525  
 
 526  13
         if ( changelogList == null )
 527  
         {
 528  3
             if ( offline )
 529  
             {
 530  0
                 throw new MavenReportException( "This report requires online mode." );
 531  
             }
 532  
 
 533  3
             getLog().info( "Generating changed sets xml to: " + outputXML.getAbsolutePath() );
 534  
 
 535  3
             changelogList = generateChangeSetsFromSCM();
 536  
 
 537  
             try
 538  
             {
 539  0
                 writeChangelogXml( changelogList );
 540  
             }
 541  0
             catch ( FileNotFoundException e )
 542  
             {
 543  0
                 throw new MavenReportException( "Can't create " + outputXML.getAbsolutePath(), e );
 544  
             }
 545  0
             catch ( UnsupportedEncodingException e )
 546  
             {
 547  0
                 throw new MavenReportException( "Can't create " + outputXML.getAbsolutePath(), e );
 548  
             }
 549  0
             catch ( IOException e )
 550  
             {
 551  0
                 throw new MavenReportException( "Can't create " + outputXML.getAbsolutePath(), e );
 552  0
             }
 553  
         }
 554  
 
 555  10
         return changelogList;
 556  
     }
 557  
 
 558  
     private void writeChangelogXml( List changelogList )
 559  
         throws FileNotFoundException, UnsupportedEncodingException, IOException
 560  
     {
 561  0
         StringBuffer changelogXml = new StringBuffer();
 562  
 
 563  0
         changelogXml.append( "<?xml version=\"1.0\" encoding=\"" ).append( outputEncoding ).append( "\"?>\n" );
 564  0
         changelogXml.append( "<changelog>" );
 565  
 
 566  0
         for ( Iterator sets = changelogList.iterator(); sets.hasNext(); )
 567  
         {
 568  0
             changelogXml.append( "\n  " );
 569  
 
 570  0
             ChangeLogSet changelogSet = (ChangeLogSet) sets.next();
 571  0
             String changeset = changelogSet.toXML( outputEncoding );
 572  
 
 573  
             //remove xml header
 574  0
             if ( changeset.startsWith( "<?xml" ) )
 575  
             {
 576  0
                 int idx = changeset.indexOf( "?>" ) + 2;
 577  0
                 changeset = changeset.substring( idx );
 578  
             }
 579  
 
 580  0
             changelogXml.append( changeset );
 581  0
         }
 582  
 
 583  0
         changelogXml.append( "\n</changelog>" );
 584  
 
 585  0
         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  0
         Writer writer = WriterFactory.newWriter( new BufferedOutputStream( new FileOutputStream( outputXML ) ), outputEncoding );
 593  0
         writer.write( changelogXml.toString() );
 594  0
         writer.flush();
 595  0
         writer.close();
 596  0
     }
 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  3
             List changeSets = new ArrayList();
 610  
 
 611  3
             ScmRepository repository = getScmRepository();
 612  
 
 613  2
             ScmProvider provider = manager.getProviderByRepository( repository );
 614  
 
 615  
             ChangeLogScmResult result;
 616  
 
 617  2
             if ( "range".equals( type ) )
 618  
             {
 619  1
                 result = provider.changeLog( repository, new ScmFileSet( basedir ), null, null, range, (ScmBranch) null,
 620  
                                              dateFormat );
 621  
 
 622  1
                 checkResult( result );
 623  
 
 624  0
                 changeSets.add( result.getChangeLog() );
 625  
             }
 626  1
             else if ( "tag".equals( type ) )
 627  
             {
 628  0
                 if ( repository.getProvider().equals( "svn" ) )
 629  
                 {
 630  0
                     throw new MavenReportException( "The type '" + type + "' isn't supported for svn." );
 631  
                 }
 632  
 
 633  0
                 Iterator tagsIter = tags.iterator();
 634  
 
 635  0
                 String startTag = (String) tagsIter.next();
 636  0
                 String endTag = null;
 637  
 
 638  0
                 if ( tagsIter.hasNext() )
 639  
                 {
 640  0
                     while ( tagsIter.hasNext() )
 641  
                     {
 642  0
                         endTag = (String) tagsIter.next();
 643  
 
 644  0
                         result = provider.changeLog( repository, new ScmFileSet( basedir ), new ScmRevision( startTag ),
 645  
                                                      new ScmRevision( endTag ) );
 646  
 
 647  0
                         checkResult( result );
 648  
 
 649  0
                         changeSets.add( result.getChangeLog() );
 650  
 
 651  0
                         startTag = endTag;
 652  
                     }
 653  
                 }
 654  
                 else
 655  
                 {
 656  0
                     result = provider.changeLog( repository, new ScmFileSet( basedir ), new ScmRevision( startTag ),
 657  
                                                  new ScmRevision( endTag ) );
 658  
 
 659  0
                     checkResult( result );
 660  
 
 661  0
                     changeSets.add( result.getChangeLog() );
 662  
                 }
 663  0
             }
 664  1
             else if ( "date".equals( type ) )
 665  
             {
 666  1
                 Iterator dateIter = dates.iterator();
 667  
 
 668  1
                 String startDate = (String) dateIter.next();
 669  1
                 String endDate = null;
 670  
 
 671  1
                 if ( dateIter.hasNext() )
 672  
                 {
 673  0
                     while ( dateIter.hasNext() )
 674  
                     {
 675  0
                         endDate = (String) dateIter.next();
 676  
 
 677  0
                         result = provider.changeLog( repository, new ScmFileSet( basedir ), parseDate( startDate ),
 678  
                                                      parseDate( endDate ), 0, (ScmBranch) null );
 679  
 
 680  0
                         checkResult( result );
 681  
 
 682  0
                         changeSets.add( result.getChangeLog() );
 683  
 
 684  0
                         startDate = endDate;
 685  
                     }
 686  
                 }
 687  
                 else
 688  
                 {
 689  1
                     result = provider.changeLog( repository, new ScmFileSet( basedir ), parseDate( startDate ),
 690  
                                                  parseDate( endDate ), 0, (ScmBranch) null );
 691  
 
 692  0
                     checkResult( result );
 693  
 
 694  0
                     changeSets.add( result.getChangeLog() );
 695  
                 }
 696  0
             }
 697  
             else
 698  
             {
 699  0
                 throw new MavenReportException( "The type '" + type + "' isn't supported." );
 700  
             }
 701  
 
 702  0
             return changeSets;
 703  
         }
 704  1
         catch ( ScmException e )
 705  
         {
 706  1
             throw new MavenReportException( "Cannot run changelog command : ", e );
 707  
         }
 708  2
         catch ( MojoExecutionException e )
 709  
         {
 710  2
             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  1
         if ( date == null || date.trim().length() == 0 )
 723  
         {
 724  0
             return null;
 725  
         }
 726  
 
 727  1
         SimpleDateFormat formatter = new SimpleDateFormat( "yyyy-MM-dd" );
 728  
 
 729  
         try
 730  
         {
 731  1
             return formatter.parse( date );
 732  
         }
 733  1
         catch ( ParseException e )
 734  
         {
 735  1
             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  3
             repository = manager.makeScmRepository( getConnection() );
 747  
 
 748  2
             ScmProviderRepository providerRepo = repository.getProviderRepository();
 749  
 
 750  2
             if ( !StringUtils.isEmpty( username ) )
 751  
             {
 752  0
                 providerRepo.setUser( username );
 753  
             }
 754  
 
 755  2
             if ( !StringUtils.isEmpty( password ) )
 756  
             {
 757  0
                 providerRepo.setPassword( password );
 758  
             }
 759  
 
 760  2
             if ( repository.getProviderRepository() instanceof ScmProviderRepositoryWithHost )
 761  
             {
 762  0
                 ScmProviderRepositoryWithHost repo = (ScmProviderRepositoryWithHost) repository.getProviderRepository();
 763  
 
 764  0
                 loadInfosFromSettings( repo );
 765  
 
 766  0
                 if ( !StringUtils.isEmpty( username ) )
 767  
                 {
 768  0
                     repo.setUser( username );
 769  
                 }
 770  
 
 771  0
                 if ( !StringUtils.isEmpty( password ) )
 772  
                 {
 773  0
                     repo.setPassword( password );
 774  
                 }
 775  
 
 776  0
                 if ( !StringUtils.isEmpty( privateKey ) )
 777  
                 {
 778  0
                     repo.setPrivateKey( privateKey );
 779  
                 }
 780  
 
 781  0
                 if ( !StringUtils.isEmpty( passphrase ) )
 782  
                 {
 783  0
                     repo.setPassphrase( passphrase );
 784  
                 }
 785  
             }
 786  
 
 787  2
             if ( !StringUtils.isEmpty( tagBase ) && repository.getProvider().equals( "svn" ) )
 788  
             {
 789  0
                 SvnScmProviderRepository svnRepo = (SvnScmProviderRepository) repository.getProviderRepository();
 790  
 
 791  0
                 svnRepo.setTagBase( tagBase );
 792  
             }
 793  
         }
 794  1
         catch ( Exception e )
 795  
         {
 796  1
             throw new ScmException( "Can't load the scm provider.", e );
 797  2
         }
 798  
 
 799  2
         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  0
         if ( username == null || password == null )
 810  
         {
 811  0
             String host = repo.getHost();
 812  
 
 813  0
             int port = repo.getPort();
 814  
 
 815  0
             if ( port > 0 )
 816  
             {
 817  0
                 host += ":" + port;
 818  
             }
 819  
 
 820  0
             Server server = this.settings.getServer( host );
 821  
 
 822  0
             if ( server != null )
 823  
             {
 824  0
                 if ( username == null )
 825  
                 {
 826  0
                     username = this.settings.getServer( host ).getUsername();
 827  
                 }
 828  
 
 829  0
                 if ( password == null )
 830  
                 {
 831  0
                     password = this.settings.getServer( host ).getPassword();
 832  
                 }
 833  
 
 834  0
                 if ( privateKey == null )
 835  
                 {
 836  0
                     privateKey = this.settings.getServer( host ).getPrivateKey();
 837  
                 }
 838  
 
 839  0
                 if ( passphrase == null )
 840  
                 {
 841  0
                     passphrase = this.settings.getServer( host ).getPassphrase();
 842  
                 }
 843  
             }
 844  
         }
 845  0
     }
 846  
 
 847  
     public void checkResult( ScmResult result )
 848  
         throws MojoExecutionException
 849  
     {
 850  1
         if ( !result.isSuccess() )
 851  
         {
 852  1
             getLog().error( "Provider message:" );
 853  
 
 854  1
             getLog().error( result.getProviderMessage() == null ? "" : result.getProviderMessage() );
 855  
 
 856  1
             getLog().error( "Command output:" );
 857  
 
 858  1
             getLog().error( result.getCommandOutput() == null ? "" : result.getCommandOutput() );
 859  
 
 860  1
             throw new MojoExecutionException( "Command failed." );
 861  
         }
 862  0
     }
 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  13
         if ( this.connection != null )
 874  
         {
 875  8
             return connection;
 876  
         }
 877  
 
 878  5
         if ( project.getScm() == null )
 879  
         {
 880  0
             throw new MavenReportException( "SCM Connection is not set." );
 881  
         }
 882  
 
 883  5
         String scmConnection = project.getScm().getConnection();
 884  5
         if ( StringUtils.isNotEmpty( scmConnection ) && "connection".equals( connectionType.toLowerCase() ) )
 885  
         {
 886  4
             connection = scmConnection;
 887  
         }
 888  
 
 889  5
         String scmDeveloper = project.getScm().getDeveloperConnection();
 890  5
         if ( StringUtils.isNotEmpty( scmDeveloper ) && "developerconnection".equals( connectionType.toLowerCase() ) )
 891  
         {
 892  0
             connection = scmDeveloper;
 893  
         }
 894  
 
 895  5
         if ( StringUtils.isEmpty( connection ) )
 896  
         {
 897  1
             throw new MavenReportException( "SCM Connection is not set." );
 898  
         }
 899  
 
 900  4
         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  14
         if ( "range".equals( type ) )
 912  
         {
 913  8
             if ( range == -1 )
 914  
             {
 915  8
                 range = DEFAULT_RANGE;
 916  
             }
 917  
         }
 918  6
         else if ( "date".equals( type ) )
 919  
         {
 920  3
             if ( dates == null )
 921  
             {
 922  0
                 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  3
         else if ( "tag".equals( type ) )
 928  
         {
 929  2
             if ( tags == null )
 930  
             {
 931  0
                 throw new MavenReportException( "The tags parameter is required when type=\"tag\"." );
 932  
             }
 933  
         }
 934  
         else
 935  
         {
 936  1
             throw new MavenReportException( "The type parameter has an invalid value: " + type
 937  
                 + ".  The value should be \"range\", \"date\", or \"tag\"." );
 938  
         }
 939  13
     }
 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  1
         sink.head();
 950  1
         sink.title();
 951  1
         sink.text( bundle.getString( "report.changelog.header" ) );
 952  1
         sink.title_();
 953  1
         sink.head_();
 954  
 
 955  1
         sink.body();
 956  1
         sink.section1();
 957  
 
 958  1
         sink.sectionTitle1();
 959  1
         sink.text( bundle.getString( "report.changelog.mainTitle" ) );
 960  1
         sink.sectionTitle1_();
 961  
 
 962  1
         sink.paragraph();
 963  1
         sink.text( bundle.getString( "report.changelog.nosources" ) );
 964  1
         sink.paragraph_();
 965  
 
 966  1
         sink.section1_();
 967  
 
 968  1
         sink.body_();
 969  1
         sink.flush();
 970  1
         sink.close();
 971  1
     }
 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  8
         sink.head();
 983  8
         sink.title();
 984  8
         sink.text( bundle.getString( "report.changelog.header" ) );
 985  8
         sink.title_();
 986  8
         sink.head_();
 987  
 
 988  8
         sink.body();
 989  8
         sink.section1();
 990  
 
 991  8
         sink.sectionTitle1();
 992  8
         sink.text( bundle.getString( "report.changelog.mainTitle" ) );
 993  8
         sink.sectionTitle1_();
 994  
 
 995  
         // Summary section
 996  8
         doSummarySection( changeLogSets, bundle, sink );
 997  
 
 998  8
         for ( Iterator sets = changeLogSets.iterator(); sets.hasNext(); )
 999  
         {
 1000  9
             ChangeLogSet changeLogSet = (ChangeLogSet) sets.next();
 1001  
 
 1002  9
             doChangedSet( changeLogSet, bundle, sink );
 1003  9
         }
 1004  
 
 1005  8
         sink.section1_();
 1006  8
         sink.body_();
 1007  
 
 1008  8
         sink.flush();
 1009  8
         sink.close();
 1010  8
     }
 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  8
         sink.paragraph();
 1022  
 
 1023  8
         sink.text( bundle.getString( "report.changelog.ChangedSetsTotal" ) );
 1024  8
         sink.text( ": " + changeLogSets.size() );
 1025  
 
 1026  8
         sink.paragraph_();
 1027  8
     }
 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  9
         sink.section2();
 1039  
 
 1040  9
         doChangeSetTitle( set, bundle, sink );
 1041  
 
 1042  9
         doSummary( set, bundle, sink );
 1043  
 
 1044  9
         doChangedSetTable( set.getChangeSets(), bundle, sink );
 1045  
 
 1046  9
         sink.section2_();
 1047  9
     }
 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  13
         sink.sectionTitle2();
 1059  
 
 1060  13
         SimpleDateFormat headingDateFormater = new SimpleDateFormat( headingDateFormat );
 1061  
 
 1062  13
         if ( "tag".equals( type ) )
 1063  
         {
 1064  2
             if ( set.getStartVersion() == null || set.getStartVersion().getName() == null )
 1065  
             {
 1066  2
                 sink.text( bundle.getString( "report.SetTagCreation" ) );
 1067  2
                 if ( set.getEndVersion() != null && set.getEndVersion().getName() != null )
 1068  
                 {
 1069  0
                     sink.text( " " + bundle.getString( "report.SetTagUntil" ) + " '" + set.getEndVersion() + "'" );
 1070  
                 }
 1071  
             }
 1072  0
             else if ( set.getEndVersion() == null || set.getEndVersion().getName() == null )
 1073  
             {
 1074  0
                 sink.text( bundle.getString( "report.SetTagSince" ) );
 1075  0
                 sink.text( " '" + set.getStartVersion() + "'" );
 1076  
             }
 1077  
             else
 1078  
             {
 1079  0
                 sink.text( bundle.getString( "report.SetTagBetween" ) );
 1080  0
                 sink.text( " '" + set.getStartVersion() + "' " + bundle.getString( "report.And" ) + " '"
 1081  
                     + set.getEndVersion() + "'" );
 1082  
             }
 1083  
         }
 1084  11
         else  if ( set.getStartDate() == null )
 1085  
         {
 1086  0
             sink.text( bundle.getString( "report.SetRangeUnknown" ) );
 1087  
         }
 1088  11
         else if ( set.getEndDate() == null )
 1089  
         {
 1090  0
             sink.text( bundle.getString( "report.SetRangeSince" ) );
 1091  0
             sink.text( " " + headingDateFormater.format( set.getStartDate() ) );
 1092  
         }
 1093  
         else
 1094  
         {
 1095  11
             sink.text( bundle.getString( "report.SetRangeBetween" ) );
 1096  11
             sink.text( " " + headingDateFormater.format( set.getStartDate() )
 1097  
                 + " " + bundle.getString( "report.And" ) + " "
 1098  
                 + headingDateFormater.format( set.getEndDate() ) );
 1099  
         }
 1100  13
         sink.sectionTitle2_();
 1101  13
     }
 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  13
         sink.paragraph();
 1113  13
         sink.text( bundle.getString( "report.TotalCommits" ) );
 1114  13
         sink.text( ": " + set.getChangeSets().size() );
 1115  13
         sink.lineBreak();
 1116  13
         sink.text( bundle.getString( "report.changelog.FilesChanged" ) );
 1117  13
         sink.text( ": " + countFilesChanged( set.getChangeSets() ) );
 1118  13
         sink.paragraph_();
 1119  13
     }
 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  13
         if ( entries == null )
 1130  
         {
 1131  0
             return 0;
 1132  
         }
 1133  
 
 1134  13
         if ( entries.isEmpty() )
 1135  
         {
 1136  7
             return 0;
 1137  
         }
 1138  
 
 1139  6
         HashMap fileList = new HashMap();
 1140  
 
 1141  6
         for ( Iterator i = entries.iterator(); i.hasNext(); )
 1142  
         {
 1143  12
             ChangeSet entry = (ChangeSet) i.next();
 1144  
 
 1145  12
             List files = entry.getFiles();
 1146  
 
 1147  12
             for ( Iterator fileIterator = files.iterator(); fileIterator.hasNext(); )
 1148  
             {
 1149  18
                 ChangeFile file = (ChangeFile) fileIterator.next();
 1150  
 
 1151  18
                 String name = file.getName();
 1152  
 
 1153  18
                 if ( fileList.containsKey( name ) )
 1154  
                 {
 1155  6
                     LinkedList list = (LinkedList) fileList.get( name );
 1156  
 
 1157  6
                     list.add( file );
 1158  6
                 }
 1159  
                 else
 1160  
                 {
 1161  12
                     LinkedList list = new LinkedList();
 1162  
 
 1163  12
                     list.add( file );
 1164  
 
 1165  12
                     fileList.put( name, list );
 1166  
                 }
 1167  18
             }
 1168  12
         }
 1169  
 
 1170  6
         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  9
         sink.table();
 1183  
 
 1184  9
         sink.tableRow();
 1185  9
         sink.tableHeaderCell();
 1186  9
         sink.text( bundle.getString( "report.changelog.timestamp" ) );
 1187  9
         sink.tableHeaderCell_();
 1188  9
         sink.tableHeaderCell();
 1189  9
         sink.text( bundle.getString( "report.changelog.author" ) );
 1190  9
         sink.tableHeaderCell_();
 1191  9
         sink.tableHeaderCell();
 1192  9
         sink.text( bundle.getString( "report.changelog.details" ) );
 1193  9
         sink.tableHeaderCell_();
 1194  9
         sink.tableRow_();
 1195  
 
 1196  9
         initReportUrls();
 1197  
 
 1198  9
         List sortedEntries = new ArrayList( entries );
 1199  9
         Collections.sort( sortedEntries, new Comparator()
 1200  
         {
 1201  9
             public int compare( Object arg0, Object arg1 )
 1202  
             {
 1203  2
                 ChangeSet changeSet0 = (ChangeSet) arg0;
 1204  2
                 ChangeSet changeSet1 = (ChangeSet) arg1;
 1205  2
                 return changeSet1.getDate().compareTo( changeSet0.getDate() );
 1206  
             }
 1207  
         } );
 1208  
 
 1209  9
         for ( Iterator i = sortedEntries.iterator(); i.hasNext(); )
 1210  
         {
 1211  4
             ChangeSet entry = (ChangeSet) i.next();
 1212  
 
 1213  4
             doChangedSetDetail( entry, bundle, sink );
 1214  4
         }
 1215  
 
 1216  9
         sink.table_();
 1217  9
     }
 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  4
         sink.tableRow();
 1229  
 
 1230  4
         sink.tableCell();
 1231  4
         sink.text( entry.getDateFormatted() + " " + entry.getTimeFormatted() );
 1232  4
         sink.tableCell_();
 1233  
 
 1234  4
         sink.tableCell();
 1235  
 
 1236  4
         sinkAuthorDetails( sink, entry.getAuthor() );
 1237  
 
 1238  4
         sink.tableCell_();
 1239  
 
 1240  4
         sink.tableCell();
 1241  
         //doRevision( entry.getFiles(), bundle, sink );
 1242  4
         doChangedFiles( entry.getFiles(), sink );
 1243  4
         sink.lineBreak();
 1244  4
         StringReader sr = new StringReader( entry.getComment() );
 1245  4
         BufferedReader br = new BufferedReader( sr );
 1246  
         String line;
 1247  
         try
 1248  
         {
 1249  4
             if ( ( issueIDRegexPattern != null && issueIDRegexPattern.length() > 0 )
 1250  
                 && ( issueLinkUrl != null && issueLinkUrl.length() > 0 ) )
 1251  
             {
 1252  0
                 Pattern pattern = Pattern.compile( issueIDRegexPattern );
 1253  
 
 1254  0
                 line = br.readLine();
 1255  
 
 1256  0
                 while ( line != null )
 1257  
                 {
 1258  0
                     sinkIssueLink( sink, line, pattern );
 1259  
 
 1260  0
                     line = br.readLine();
 1261  0
                     if ( line != null )
 1262  
                     {
 1263  0
                         sink.lineBreak();
 1264  
                     }
 1265  
                 }
 1266  0
             }
 1267  
             else
 1268  
             {
 1269  4
                 line = br.readLine();
 1270  
 
 1271  8
                 while ( line != null )
 1272  
                 {
 1273  4
                     sink.text( line );
 1274  4
                     line = br.readLine();
 1275  4
                     if ( line != null )
 1276  
                     {
 1277  0
                         sink.lineBreak();
 1278  
                     }
 1279  
                 }
 1280  
             }
 1281  
         }
 1282  0
         catch ( IOException e )
 1283  
         {
 1284  0
             getLog().warn( "Unable to read the comment of a ChangeSet as a stream." );
 1285  
         }
 1286  
         finally
 1287  
         {
 1288  4
             if ( br != null )
 1289  
             {
 1290  
                 try
 1291  
                 {
 1292  4
                     br.close();
 1293  
                 }
 1294  0
                 catch ( IOException e )
 1295  
                 {
 1296  0
                     getLog().warn( "Unable to close a reader." );
 1297  4
                 }
 1298  
             }
 1299  4
             if ( sr != null )
 1300  
             {
 1301  4
                 sr.close();
 1302  
             }
 1303  
         }
 1304  4
         sink.tableCell_();
 1305  
 
 1306  4
         sink.tableRow_();
 1307  4
     }
 1308  
 
 1309  
     private void sinkIssueLink( Sink sink, String line, Pattern pattern )
 1310  
     {
 1311  
         // replace any ticket patterns found.
 1312  
 
 1313  0
         Matcher matcher = pattern.matcher( line );
 1314  
 
 1315  0
         int currLoc = 0;
 1316  
 
 1317  0
         while ( matcher.find() )
 1318  
         {
 1319  0
             String match = matcher.group();
 1320  
 
 1321  
             String link;
 1322  
 
 1323  0
             if ( issueLinkUrl.indexOf( ISSUE_TOKEN ) > 0 )
 1324  
             {
 1325  0
                 link = issueLinkUrl.replaceAll( ISSUE_TOKEN, match );
 1326  
             }
 1327  
             else
 1328  
             {
 1329  0
                 if ( issueLinkUrl.endsWith( "/" ) )
 1330  
                 {
 1331  0
                     link = issueLinkUrl;
 1332  
                 }
 1333  
                 else
 1334  
                 {
 1335  0
                     link = issueLinkUrl + "/";
 1336  
                 }
 1337  
 
 1338  0
                 link += match;
 1339  
             }
 1340  
 
 1341  0
             int startOfMatch = matcher.start();
 1342  
 
 1343  0
             String unmatchedText = line.substring( currLoc, startOfMatch );
 1344  
 
 1345  0
             currLoc = matcher.end();
 1346  
 
 1347  0
             sink.text( unmatchedText );
 1348  
 
 1349  0
             sink.link( link );
 1350  0
             sink.text( match );
 1351  0
             sink.link_();
 1352  0
         }
 1353  
 
 1354  0
         sink.text( line.substring( currLoc ) );
 1355  0
     }
 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  6
         Developer developer = (Developer) developersById.get( author );
 1368  
 
 1369  6
         if ( developer == null )
 1370  
         {
 1371  6
             developer = (Developer) developersByName.get( author );
 1372  
         }
 1373  
 
 1374  6
         if ( developer != null )
 1375  
         {
 1376  2
             sink.link( "team-list.html#" + developer.getId() );
 1377  2
             sink.text( developer.getName() );
 1378  2
             sink.link_();
 1379  
         }
 1380  
         else
 1381  
         {
 1382  4
             sink.text( author );
 1383  
         }
 1384  6
     }
 1385  
 
 1386  
     /**
 1387  
      * populates the report url used to create links from certain elements of the report
 1388  
      */
 1389  
     protected void initReportUrls()
 1390  
     {
 1391  11
         if ( scmUrl != null )
 1392  
         {
 1393  11
             int idx = scmUrl.indexOf( '?' );
 1394  
 
 1395  11
             if ( idx > 0 )
 1396  
             {
 1397  1
                 rptRepository = scmUrl.substring( 0, idx );
 1398  
 
 1399  1
                 if ( scmUrl.equals( displayFileDetailUrl ) )
 1400  
                 {
 1401  1
                     String rptTmpMultiRepoParam = scmUrl.substring( rptRepository.length() );
 1402  
 
 1403  1
                     rptOneRepoParam = "?" + rptTmpMultiRepoParam.substring( 1 );
 1404  
 
 1405  1
                     rptMultiRepoParam = "&" + rptTmpMultiRepoParam.substring( 1 );
 1406  1
                 }
 1407  
             }
 1408  
             else
 1409  
             {
 1410  10
                 rptRepository = scmUrl;
 1411  
 
 1412  10
                 rptOneRepoParam = "";
 1413  
 
 1414  10
                 rptMultiRepoParam = "";
 1415  
             }
 1416  
         }
 1417  11
     }
 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  4
         for ( Iterator i = files.iterator(); i.hasNext(); )
 1428  
         {
 1429  6
             ChangeFile file = (ChangeFile) i.next();
 1430  6
             sinkLogFile( sink, file.getName(), file.getRevision() );
 1431  6
         }
 1432  4
     }
 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  6
             String connection = getConnection();
 1446  
 
 1447  6
             generateLinks( connection, name, revision, sink );
 1448  
         }
 1449  0
         catch ( Exception e )
 1450  
         {
 1451  0
             getLog().debug( e );
 1452  
 
 1453  0
             sink.text( name + " v " + revision );
 1454  6
         }
 1455  
 
 1456  6
         sink.lineBreak();
 1457  6
     }
 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  4
         generateLinks( connection, name, null, sink );
 1469  4
     }
 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  10
         String linkFile = null;
 1482  10
         String linkRev = null;
 1483  
 
 1484  10
         if ( revision != null )
 1485  
         {
 1486  6
             linkFile = displayFileRevDetailUrl;
 1487  
         }
 1488  
         else
 1489  
         {
 1490  4
             linkFile = displayFileDetailUrl;
 1491  
         }
 1492  
 
 1493  10
         if ( linkFile != null )
 1494  
         {
 1495  0
             if ( !scmUrl.equals( linkFile ) )
 1496  
             {
 1497  
                 // Use the given URL to create links to the files
 1498  
 
 1499  0
                 if ( linkFile.indexOf( FILE_TOKEN ) > 0 )
 1500  
                 {
 1501  0
                     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  0
                     linkFile = linkFile + name;
 1509  
                 }
 1510  
 
 1511  
                 // Use the given URL to create links to the files
 1512  
 
 1513  0
                 if (  revision != null && linkFile.indexOf( REV_TOKEN ) > 0 )
 1514  
                 {
 1515  0
                     linkFile = linkFile.replaceAll( REV_TOKEN, revision );
 1516  
                 }
 1517  
             }
 1518  0
             else if ( connection.startsWith( "scm:perforce" ) )
 1519  
             {
 1520  0
                 String path = getAbsolutePath( displayFileDetailUrl, name );
 1521  0
                 linkFile = path + "?ac=22";
 1522  0
                 if ( revision != null )
 1523  
                 {
 1524  0
                     linkRev = path + "?ac=64&rev=" + revision;
 1525  
                 }
 1526  0
             }
 1527  0
             else if ( connection.startsWith( "scm:clearcase" ) )
 1528  
             {
 1529  0
                 String path = getAbsolutePath( displayFileDetailUrl, name );
 1530  0
                 linkFile = path + rptOneRepoParam;
 1531  0
             }
 1532  0
             else if ( connection.indexOf( "cvsmonitor.pl" ) > 0 )
 1533  
             {
 1534  0
                 String module = rptOneRepoParam.replaceAll( "^.*(&amp;module=.*?(?:&amp;|$)).*$", "$1" );
 1535  0
                 linkFile = displayFileDetailUrl + "?cmd=viewBrowseFile" + module + "&file=" + name;
 1536  0
                 if ( revision != null )
 1537  
                 {
 1538  0
                     linkRev =
 1539  
                         rptRepository + "?cmd=viewBrowseVersion" + module + "&file=" + name + "&version=" + revision;
 1540  
                 }
 1541  0
             }
 1542  
             else
 1543  
             {
 1544  0
                 String path = getAbsolutePath( displayFileDetailUrl, name );
 1545  0
                 linkFile = path + rptOneRepoParam;
 1546  0
                 if ( revision != null )
 1547  
                 {
 1548  0
                     linkRev = path + "?rev=" + revision + "&content-type=text/vnd.viewcvs-markup" + rptMultiRepoParam;
 1549  
                 }
 1550  
             }
 1551  
         }
 1552  
 
 1553  10
         if ( linkFile != null )
 1554  
         {
 1555  0
             sink.link( linkFile );
 1556  0
             sinkFileName( name, sink );
 1557  0
             sink.link_();
 1558  
         }
 1559  
         else
 1560  
         {
 1561  10
             sinkFileName( name, sink );
 1562  
         }
 1563  
 
 1564  10
         sink.text( " " );
 1565  
 
 1566  10
         if ( linkRev == null && revision != null && displayChangeSetDetailUrl != null )
 1567  
         {
 1568  0
             if ( displayChangeSetDetailUrl.indexOf( REV_TOKEN ) > 0 )
 1569  
             {
 1570  0
                 linkRev = displayChangeSetDetailUrl.replaceAll( REV_TOKEN, revision );
 1571  
             }
 1572  
             else
 1573  
             {
 1574  0
                 linkRev = displayChangeSetDetailUrl + revision;
 1575  
             }
 1576  
         }
 1577  
 
 1578  10
         if ( linkRev != null )
 1579  
         {
 1580  0
             sink.link( linkRev );
 1581  0
             sink.text( "v " + revision );
 1582  0
             sink.link_();
 1583  
         }
 1584  10
         else if ( revision != null )
 1585  
         {
 1586  6
             sink.text( "v " + revision );
 1587  
         }
 1588  10
     }
 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  10
         name = name.replaceAll( "\\\\", "/" );
 1599  10
         int pos = name.lastIndexOf( '/' );
 1600  
 
 1601  
         String head;
 1602  
         String tail;
 1603  10
         if ( pos < 0 )
 1604  
         {
 1605  0
             head = "";
 1606  0
             tail = name;
 1607  
         }
 1608  
         else
 1609  
         {
 1610  10
             head = name.substring( 0, pos ) + "/";
 1611  10
             tail = name.substring( pos + 1 );
 1612  
         }
 1613  
 
 1614  10
         sink.text( head );
 1615  10
         sink.bold();
 1616  10
         sink.text( tail );
 1617  10
         sink.bold_();
 1618  10
     }
 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  0
         String absPath = "";
 1629  
 
 1630  0
         StringTokenizer baseTokens = new StringTokenizer( base.replaceAll( "\\\\", "/" ), "/", true );
 1631  
 
 1632  0
         StringTokenizer targetTokens = new StringTokenizer( target.replaceAll( "\\\\", "/" ), "/" );
 1633  
 
 1634  0
         String targetRoot = targetTokens.nextToken();
 1635  
 
 1636  0
         while ( baseTokens.hasMoreTokens() )
 1637  
         {
 1638  0
             String baseToken = baseTokens.nextToken();
 1639  
 
 1640  0
             if ( baseToken.equals( targetRoot ) )
 1641  
             {
 1642  0
                 break;
 1643  
             }
 1644  
 
 1645  0
             absPath += baseToken;
 1646  0
         }
 1647  
 
 1648  0
         if ( !absPath.endsWith( "/" ) )
 1649  
         {
 1650  0
             absPath += "/";
 1651  
         }
 1652  
 
 1653  0
         String newTarget = target;
 1654  0
         if ( newTarget.startsWith( "/" ) )
 1655  
         {
 1656  0
             newTarget = newTarget.substring( 1 );
 1657  
         }
 1658  
 
 1659  0
         return absPath + newTarget;
 1660  
     }
 1661  
 
 1662  
     /** {@inheritDoc} */
 1663  
     protected MavenProject getProject()
 1664  
     {
 1665  0
         return project;
 1666  
     }
 1667  
 
 1668  
     /** {@inheritDoc} */
 1669  
     protected String getOutputDirectory()
 1670  
     {
 1671  17
         if ( !outputDirectory.isAbsolute() )
 1672  
         {
 1673  17
             outputDirectory = new File( project.getBasedir(), outputDirectory.getPath() );
 1674  
         }
 1675  
 
 1676  17
         return outputDirectory.getAbsolutePath();
 1677  
     }
 1678  
 
 1679  
     /** {@inheritDoc} */
 1680  
     protected Renderer getSiteRenderer()
 1681  
     {
 1682  26
         return siteRenderer;
 1683  
     }
 1684  
 
 1685  
     /** {@inheritDoc} */
 1686  
     public String getDescription( Locale locale )
 1687  
     {
 1688  0
         return getBundle( locale ).getString( "report.changelog.description" );
 1689  
     }
 1690  
 
 1691  
     /** {@inheritDoc} */
 1692  
     public String getName( Locale locale )
 1693  
     {
 1694  4
         return getBundle( locale ).getString( "report.changelog.name" );
 1695  
     }
 1696  
 
 1697  
     /** {@inheritDoc} */
 1698  
     public String getOutputName()
 1699  
     {
 1700  13
         return "changelog";
 1701  
     }
 1702  
 
 1703  
     /**
 1704  
      * @param locale
 1705  
      * @return the current bundle
 1706  
      */
 1707  
     protected ResourceBundle getBundle( Locale locale )
 1708  
     {
 1709  17
         return ResourceBundle.getBundle( "scm-activity", locale, this.getClass().getClassLoader() );
 1710  
     }
 1711  
 
 1712  
     /** {@inheritDoc} */
 1713  
     public boolean canGenerateReport()
 1714  
     {
 1715  0
         if ( offline && !outputXML.exists() )
 1716  
         {
 1717  0
             return false;
 1718  
         }
 1719  
 
 1720  0
         return true;
 1721  
     }
 1722  
 }