blob: 352c6b2bba006ee12268e01d957fdad0871eabff [file] [log] [blame]
package org.apache.maven.plugin.changelog;
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.siterenderer.Renderer;
import org.apache.maven.model.Developer;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.reporting.AbstractMavenReport;
import org.apache.maven.reporting.MavenReportException;
import org.apache.maven.scm.ChangeFile;
import org.apache.maven.scm.ChangeSet;
import org.apache.maven.scm.ScmBranch;
import org.apache.maven.scm.ScmException;
import org.apache.maven.scm.ScmFileSet;
import org.apache.maven.scm.ScmResult;
import org.apache.maven.scm.ScmRevision;
import org.apache.maven.scm.command.changelog.ChangeLogScmResult;
import org.apache.maven.scm.command.changelog.ChangeLogSet;
import org.apache.maven.scm.manager.ScmManager;
import org.apache.maven.scm.provider.ScmProvider;
import org.apache.maven.scm.provider.ScmProviderRepository;
import org.apache.maven.scm.provider.ScmProviderRepositoryWithHost;
import org.apache.maven.scm.provider.svn.repository.SvnScmProviderRepository;
import org.apache.maven.scm.repository.ScmRepository;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.WriterFactory;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLEncoder;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Generate a changelog report.
*
* @version $Id$
*/
@Mojo( name = "changelog" )
public class ChangeLogReport
extends AbstractMavenReport
{
/**
* A special token that represents the SCM relative path for a file.
* It can be used in <code>displayFileDetailUrl</code>.
*/
private static final String FILE_TOKEN = "%FILE%";
/**
* A special token that represents a Mantis/Bugzilla/JIRA/etc issue ID.
* It can be used in the <code>issueLinkUrl</code>.
*/
private static final String ISSUE_TOKEN = "%ISSUE%";
/**
* A special token that represents the SCM revision number.
* It can be used in <code>displayChangeSetDetailUrl</code>
* and <code>displayFileRevDetailUrl</code>.
*/
private static final String REV_TOKEN = "%REV%";
/**
* The number of days to use as a range, when this is not specified.
*/
private static final int DEFAULT_RANGE = 30;
public static final String DEFAULT_ISSUE_ID_REGEX_PATTERN = "[a-zA-Z]{2,}-\\d+";
/**
* Used to specify the format to use for the dates in the headings of the
* report.
*
* @since 2.1
*/
@Parameter( property = "changelog.headingDateFormat", defaultValue = "yyyy-MM-dd" )
private String headingDateFormat = "yyyy-MM-dd";
/**
* Used to specify whether to build the log using range, tag or date.
*/
@Parameter( property = "changelog.type", defaultValue = "range", required = true )
private String type;
/**
* Used to specify the number of days of log entries to retrieve.
*/
@Parameter( property = "changelog.range", defaultValue = "-1" )
private int range;
/**
* Used to specify the absolute date (or list of dates) to start log entries from.
*/
@Parameter
private List<String> dates;
/**
* Used to specify the tag (or list of tags) to start log entries from.
*/
@Parameter
private List<String> tags;
/**
* Used to specify the date format of the log entries that are retrieved from your SCM system.
*/
@Parameter( property = "changelog.dateFormat", defaultValue = "yyyy-MM-dd HH:mm:ss", required = true )
private String dateFormat;
/**
* Input dir. Directory where the files under SCM control are located.
*/
@Parameter( property = "basedir", required = true )
private File basedir;
/**
* Output file for xml document
*/
@Parameter( defaultValue = "${project.build.directory}/changelog.xml", required = true )
private File outputXML;
/**
* Allows the user to make changelog regenerate the changelog.xml file for the specified time in minutes.
*/
@Parameter( property = "outputXMLExpiration", defaultValue = "60", required = true )
private int outputXMLExpiration;
/**
* Comment format string used for interrogating
* the revision control system.
* Currently only used by the ClearcaseChangeLogGenerator.
*/
@Parameter( property = "changelog.commentFormat" )
private String commentFormat;
/**
* The file encoding when writing non-HTML reports.
*/
@Parameter( property = "changelog.outputEncoding", defaultValue = "${project.reporting.outputEncoding}" )
private String outputEncoding;
/**
* The user name (used by svn and starteam protocol).
*/
@Parameter( property = "username" )
private String username;
/**
* The user password (used by svn and starteam protocol).
*/
@Parameter( property = "password" )
private String password;
/**
* The private key (used by java svn).
*/
@Parameter( property = "privateKey" )
private String privateKey;
/**
* The passphrase (used by java svn).
*/
@Parameter( property = "passphrase" )
private String passphrase;
/**
* The url of tags base directory (used by svn protocol).
*/
@Parameter( property = "tagBase" )
private String tagBase;
/**
* The URL to view the scm. Basis for external links from the generated report.
*/
@Parameter( property = "project.scm.url" )
protected String scmUrl;
/**
* Skip the Changelog report generation. Most useful on the command line
* via "-Dchangelog.skip=true".
*
* @since 2.3
*/
@Parameter( property = "changelog.skip", defaultValue = "false" )
protected boolean skip;
/**
* Encodes slashes in file uri. Required for some repository browsers like gitblit
*
* @since 2.3
*/
@Parameter( property = "encodeFileUri", defaultValue = "false")
protected boolean encodeFileUri;
/**
* List of files to include. Specified as fileset patterns of files to include in the report
* @since 2.3
*/
@Parameter
private String[] includes;
/**
* List of files to include. Specified as fileset patterns of files to omit in the report
* @since 2.3
*/
@Parameter
private String[] excludes;
/**
* The Maven Project Object
*/
@Component
private MavenProject project;
/**
* The directory where the report will be generated
*/
@Parameter( property = "project.reporting.outputDirectory", required = true, readonly = true )
private File outputDirectory;
/**
*/
@Component
private Renderer siteRenderer;
/**
*/
@Parameter( property = "settings.offline", required = true, readonly = true )
private boolean offline;
/**
*/
@Component
private ScmManager manager;
/**
*/
@Component
private Settings settings;
/**
* Allows the user to choose which scm connection to use when connecting to the scm.
* Can either be "connection" or "developerConnection".
*/
@Parameter( defaultValue = "connection", required = true )
private String connectionType;
/**
* A template string that is used to create the URL to the file details.
* There is a special token that you can use in your template:
* <ul>
* <li><code>%FILE%</code> - this is the path to a file</li>
* </ul>
* <p>
* Example:
* <code>http://checkstyle.cvs.sourceforge.net/checkstyle%FILE%?view=markup</code>
* </p>
* <p>
* <strong>Note:</strong> If you don't supply the token in your template,
* the path of the file will simply be appended to your template URL.
* </p>
*/
@Parameter( property = "displayFileDetailUrl", defaultValue = "${project.scm.url}" )
protected String displayFileDetailUrl;
/**
* A pattern used to identify 'issue tracker' IDs such as those used by JIRA,
* Bugzilla and alike in the SCM commit messages. Any matched patterns
* are replaced with <code>issueLinkUrl<code> URL. The default
* value is a JIRA-style issue identification pattern.
* <p/>
* <strong>Note:</strong> Default value is [a-zA-Z]{2,}-\d+
* @since 2.2
*/
@Parameter( property = "issueIDRegexPattern", defaultValue = DEFAULT_ISSUE_ID_REGEX_PATTERN, required = true )
private String issueIDRegexPattern;
/**
* The issue tracker URL used when replacing any matched <code>issueIDRegexPattern</code>
* found in the SCM commit messages. The default is URL is the codehaus JIRA
* URL. If %ISSUE% is found in the URL it is replaced with the matched issue ID,
* otherwise the matched issue ID is appended to the URL.
*
* @since 2.2
*/
@Parameter( property = "issueLinkUrl", defaultValue = "http://jira.codehaus.org/browse/%ISSUE%", required = true )
private String issueLinkUrl;
/**
* A template string that is used to create the changeset URL.
* <p/>
* If not defined no change set link will be created.
* <p/>
* There is one special token that you can use in your template:
* <ul>
* <li><code>%REV%</code> - this is the changeset revision</li>
* </ul>
* <p>
* Example:
* <code>http://fisheye.sourceforge.net/changelog/a-project/?cs=%REV%</code>
* </p>
* <p>
* <strong>Note:</strong> If you don't supply the %REV% token in your template,
* the revision will simply be appended to your template URL.
* </p>
*
* @since 2.2
*/
@Parameter( property = "displayChangeSetDetailUrl" )
protected String displayChangeSetDetailUrl;
/**
* A template string that is used to create the revision aware URL to
* the file details in a similar fashion to the <code>displayFileDetailUrl</code>.
* When a report contains both file and file revision information, as in the
* Change Log report, this template string can be used to create a revision
* aware URL to the file details.
* <p/>
* If not defined this template string defaults to the same value as the
* <code>displayFileDetailUrl</code> and thus revision number aware links will
* not be used.
* <p/>
* There are two special tokens that you can use in your template:
* <ul>
* <li><code>%FILE%</code> - this is the path to a file</li>
* <li><code>%REV%</code> - this is the revision of the file</li>
* </ul>
* <p>
* Example:
* <code>http://fisheye.sourceforge.net/browse/a-project/%FILE%?r=%REV%</code>
* </p>
* <p>
* <strong>Note:</strong> If you don't supply the %FILE% token in your template,
* the path of the file will simply be appended to your template URL.
* </p>
*
* @since 2.2
*/
@Parameter( property = "displayFileRevDetailUrl" )
protected String displayFileRevDetailUrl;
/**
* List of developers to be shown on the report.
*
* @since 2.2
*/
@Parameter( property = "project.developers" )
protected List<Developer> developers;
/**
* List of provider implementations.
*/
@Parameter
private Map<String, String> providerImplementations;
// temporary field holder while generating the report
private String rptRepository, rptOneRepoParam, rptMultiRepoParam;
// field for SCM Connection URL
private String connection;
// field used to hold a map of the developers by Id
private HashMap<String, Developer> developersById;
// field used to hold a map of the developers by Name
private HashMap<String, Developer> developersByName;
/**
* The system properties to use (needed by the perforce scm provider).
*/
@Parameter
private Properties systemProperties;
/** {@inheritDoc} */
public void executeReport( Locale locale )
throws MavenReportException
{
//check if sources exists <-- required for parent poms
if ( !basedir.exists() )
{
doGenerateEmptyReport( getBundle( locale ), getSink() );
return;
}
if ( providerImplementations != null )
{
for ( Map.Entry<String, String> entry : providerImplementations.entrySet() )
{
String providerType = entry.getKey();
String providerImplementation = entry.getValue();
getLog().info( "Change the default '" + providerType + "' provider implementation to '"
+ providerImplementation + "'." );
manager.setScmProviderImplementation( providerType, providerImplementation );
}
}
initializeDefaultConfigurationParameters();
initializeDeveloperMaps();
verifySCMTypeParams();
if ( systemProperties != null )
{
// Add all system properties configured by the user
Iterator<?> iter = systemProperties.keySet().iterator();
while ( iter.hasNext() )
{
String key = (String) iter.next();
String value = systemProperties.getProperty( key );
System.setProperty( key, value );
getLog().debug( "Setting system property: " + key + "=" + value );
}
}
doGenerateReport( getChangedSets(), getBundle( locale ), getSink() );
}
/**
* Initializes any configuration parameters that have not/can not be defined
* or defaulted by the Mojo API.
*/
private void initializeDefaultConfigurationParameters()
{
if ( displayFileRevDetailUrl == null || displayFileRevDetailUrl.length() == 0 )
{
displayFileRevDetailUrl = displayFileDetailUrl;
}
}
/**
* Creates maps of the project developers by developer Id and developer Name
* for quick lookups.
*/
private void initializeDeveloperMaps()
{
developersById = new HashMap<String, Developer>();
developersByName = new HashMap<String, Developer>();
if ( developers != null )
{
for ( Developer developer : developers )
{
developersById.put( developer.getId(), developer );
developersByName.put( developer.getName(), developer );
}
}
}
/**
* populates the changedSets field by either connecting to the SCM or using an existing XML generated in a previous
* run of the report
*
* @throws MavenReportException
*/
protected List<ChangeLogSet> getChangedSets()
throws MavenReportException
{
List<ChangeLogSet> changelogList = null;
if ( !outputXML.isAbsolute() )
{
outputXML = new File( project.getBasedir(), outputXML.getPath() );
}
if ( outputXML.exists() )
{
if ( outputXMLExpiration > 0
&& outputXMLExpiration * 60000 > System.currentTimeMillis() - outputXML.lastModified() )
{
try
{
//ReaderFactory.newReader( outputXML, getOutputEncoding() );
//FileInputStream fIn = new FileInputStream( outputXML );
getLog().info( "Using existing changelog.xml..." );
changelogList = ChangeLog.loadChangedSets( ReaderFactory.newReader( outputXML, getOutputEncoding() ) );
}
catch ( FileNotFoundException e )
{
//do nothing, just regenerate
}
catch ( Exception e )
{
throw new MavenReportException( "An error occurred while parsing " + outputXML.getAbsolutePath(),
e );
}
}
}
if ( changelogList == null )
{
if ( offline )
{
throw new MavenReportException( "This report requires online mode." );
}
getLog().info( "Generating changed sets xml to: " + outputXML.getAbsolutePath() );
changelogList = generateChangeSetsFromSCM();
try
{
writeChangelogXml( changelogList );
}
catch ( FileNotFoundException e )
{
throw new MavenReportException( "Can't create " + outputXML.getAbsolutePath(), e );
}
catch ( UnsupportedEncodingException e )
{
throw new MavenReportException( "Can't create " + outputXML.getAbsolutePath(), e );
}
catch ( IOException e )
{
throw new MavenReportException( "Can't create " + outputXML.getAbsolutePath(), e );
}
}
return changelogList;
}
private void writeChangelogXml( List<ChangeLogSet> changelogList )
throws FileNotFoundException, UnsupportedEncodingException, IOException
{
StringBuilder changelogXml = new StringBuilder();
changelogXml.append( "<?xml version=\"1.0\" encoding=\"" ).append( getOutputEncoding() ).append( "\"?>\n" );
changelogXml.append( "<changelog>" );
for ( ChangeLogSet changelogSet : changelogList )
{
changelogXml.append( "\n " );
String changeset = changelogSet.toXML( getOutputEncoding() );
//remove xml header
if ( changeset.startsWith( "<?xml" ) )
{
int idx = changeset.indexOf( "?>" ) + 2;
changeset = changeset.substring( idx );
}
changelogXml.append( changeset );
}
changelogXml.append( "\n</changelog>" );
outputXML.getParentFile().mkdirs();
//PrintWriter pw = new PrintWriter( new BufferedOutputStream( new FileOutputStream( outputXML ) ) );
//pw.write( changelogXml.toString() );
//pw.flush();
//pw.close();
// MCHANGELOG-86
Writer writer = WriterFactory.newWriter( new BufferedOutputStream( new FileOutputStream( outputXML ) ), getOutputEncoding() );
writer.write( changelogXml.toString() );
writer.flush();
writer.close();
}
/**
* creates a ChangeLog object and then connects to the SCM to generate the changed sets
*
* @return changedlogsets generated from the SCM
* @throws MavenReportException
*/
protected List<ChangeLogSet> generateChangeSetsFromSCM()
throws MavenReportException
{
try
{
List<ChangeLogSet> changeSets = new ArrayList<ChangeLogSet>();
ScmRepository repository = getScmRepository();
ScmProvider provider = manager.getProviderByRepository( repository );
ChangeLogScmResult result;
if ( "range".equals( type ) )
{
result = provider.changeLog( repository, new ScmFileSet( basedir ), null, null, range, (ScmBranch) null,
dateFormat );
checkResult( result );
changeSets.add( result.getChangeLog() );
}
else if ( "tag".equals( type ) )
{
if ( repository.getProvider().equals( "svn" ) )
{
throw new MavenReportException( "The type '" + type + "' isn't supported for svn." );
}
Iterator<String> tagsIter = tags.iterator();
String startTag = tagsIter.next();
String endTag = null;
if ( tagsIter.hasNext() )
{
while ( tagsIter.hasNext() )
{
endTag = tagsIter.next();
result = provider.changeLog( repository, new ScmFileSet( basedir ), new ScmRevision( startTag ),
new ScmRevision( endTag ) );
checkResult( result );
changeSets.add( result.getChangeLog() );
startTag = endTag;
}
}
else
{
result = provider.changeLog( repository, new ScmFileSet( basedir ), new ScmRevision( startTag ),
new ScmRevision( endTag ) );
checkResult( result );
changeSets.add( result.getChangeLog() );
}
}
else if ( "date".equals( type ) )
{
Iterator<String> dateIter = dates.iterator();
String startDate = dateIter.next();
String endDate = null;
if ( dateIter.hasNext() )
{
while ( dateIter.hasNext() )
{
endDate = dateIter.next();
result = provider.changeLog( repository, new ScmFileSet( basedir ), parseDate( startDate ),
parseDate( endDate ), 0, (ScmBranch) null );
checkResult( result );
changeSets.add( result.getChangeLog() );
startDate = endDate;
}
}
else
{
result = provider.changeLog( repository, new ScmFileSet( basedir ), parseDate( startDate ),
parseDate( endDate ), 0, (ScmBranch) null );
checkResult( result );
changeSets.add( result.getChangeLog() );
}
}
else
{
throw new MavenReportException( "The type '" + type + "' isn't supported." );
}
filter(changeSets);
return changeSets;
}
catch ( ScmException e )
{
throw new MavenReportException( "Cannot run changelog command : ", e );
}
catch ( MojoExecutionException e )
{
throw new MavenReportException( "An error has occurred during changelog command : ", e );
}
}
/**
* filters out unwanted files from the changesets
*/
private void filter(List<ChangeLogSet> changeSets)
{
List<Pattern> include = compilePatterns(includes);
List<Pattern> exclude = compilePatterns(excludes);
if(includes==null && excludes==null)
return;
for (ChangeLogSet changeLogSet : changeSets)
{
List<ChangeSet> set = changeLogSet.getChangeSets();
filter(set, include, exclude);
}
}
private List<Pattern> compilePatterns(String[] patternArray)
{
if(patternArray==null)
return new ArrayList<Pattern>();
List<Pattern> patterns = new ArrayList<Pattern>(patternArray.length);
for (String string : patternArray)
{
//replaces * with [/\]* (everything but file seperators)
//replaces ** with .*
//quotes the rest of the string
string = "\\Q" + string + "\\E";
string = string.replace("**", "\\E.?REPLACEMENT?\\Q");
string = string.replace("*", "\\E[^/\\\\]?REPLACEMENT?\\Q");
string = string.replace("?REPLACEMENT?", "*");
string = string.replace("\\Q\\E", "");
patterns.add(Pattern.compile(string));
}
return patterns;
}
private void filter(List<ChangeSet> sets, List<Pattern> includes, List<Pattern> excludes)
{
Iterator<ChangeSet> it = sets.iterator();
while (it.hasNext())
{
ChangeSet changeSet = it.next();
List<ChangeFile> files = changeSet.getFiles();
Iterator<ChangeFile> iterator = files.iterator();
while (iterator.hasNext())
{
ChangeFile changeFile = (ChangeFile)iterator.next();
String name = changeFile.getName();
if(!isIncluded(includes,name) || isExcluded(excludes, name))
{
iterator.remove();
}
}
if(files.isEmpty())
it.remove();
}
}
private boolean isExcluded(List<Pattern> excludes, String name)
{
if(excludes==null || excludes.isEmpty())
return false;
for (Pattern pattern : excludes)
{
if(pattern.matcher(name).matches())
return true;
}
return false;
}
private boolean isIncluded(List<Pattern> includes, String name)
{
if(includes==null || includes.isEmpty())
return true;
for (Pattern pattern : includes)
{
if(pattern.matcher(name).matches())
return true;
}
return false;
}
/**
* Converts the localized date string pattern to date object.
*
* @return A date
*/
private Date parseDate( String date )
throws MojoExecutionException
{
if ( date == null || date.trim().length() == 0 )
{
return null;
}
SimpleDateFormat formatter = new SimpleDateFormat( "yyyy-MM-dd" );
try
{
return formatter.parse( date );
}
catch ( ParseException e )
{
throw new MojoExecutionException( "Please use this date pattern: " + formatter.toLocalizedPattern(), e );
}
}
public ScmRepository getScmRepository()
throws ScmException
{
ScmRepository repository;
try
{
repository = manager.makeScmRepository( getConnection() );
ScmProviderRepository providerRepo = repository.getProviderRepository();
if ( !StringUtils.isEmpty( username ) )
{
providerRepo.setUser( username );
}
if ( !StringUtils.isEmpty( password ) )
{
providerRepo.setPassword( password );
}
if ( repository.getProviderRepository() instanceof ScmProviderRepositoryWithHost )
{
ScmProviderRepositoryWithHost repo = (ScmProviderRepositoryWithHost) repository.getProviderRepository();
loadInfosFromSettings( repo );
if ( !StringUtils.isEmpty( username ) )
{
repo.setUser( username );
}
if ( !StringUtils.isEmpty( password ) )
{
repo.setPassword( password );
}
if ( !StringUtils.isEmpty( privateKey ) )
{
repo.setPrivateKey( privateKey );
}
if ( !StringUtils.isEmpty( passphrase ) )
{
repo.setPassphrase( passphrase );
}
}
if ( !StringUtils.isEmpty( tagBase ) && repository.getProvider().equals( "svn" ) )
{
SvnScmProviderRepository svnRepo = (SvnScmProviderRepository) repository.getProviderRepository();
svnRepo.setTagBase( tagBase );
}
}
catch ( Exception e )
{
throw new ScmException( "Can't load the scm provider.", e );
}
return repository;
}
/**
* Load username password from settings if user has not set them in JVM properties
*
* @param repo
*/
private void loadInfosFromSettings( ScmProviderRepositoryWithHost repo )
{
if ( username == null || password == null )
{
String host = repo.getHost();
int port = repo.getPort();
if ( port > 0 )
{
host += ":" + port;
}
Server server = this.settings.getServer( host );
if ( server != null )
{
if ( username == null )
{
username = this.settings.getServer( host ).getUsername();
}
if ( password == null )
{
password = this.settings.getServer( host ).getPassword();
}
if ( privateKey == null )
{
privateKey = this.settings.getServer( host ).getPrivateKey();
}
if ( passphrase == null )
{
passphrase = this.settings.getServer( host ).getPassphrase();
}
}
}
}
public void checkResult( ScmResult result )
throws MojoExecutionException
{
if ( !result.isSuccess() )
{
getLog().error( "Provider message:" );
getLog().error( result.getProviderMessage() == null ? "" : result.getProviderMessage() );
getLog().error( "Command output:" );
getLog().error( result.getCommandOutput() == null ? "" : result.getCommandOutput() );
throw new MojoExecutionException( "Command failed." );
}
}
/**
* used to retrieve the SCM connection string
*
* @return the url string used to connect to the SCM
* @throws MavenReportException when there is insufficient information to retrieve the SCM connection string
*/
protected String getConnection()
throws MavenReportException
{
if ( this.connection != null )
{
return connection;
}
if ( project.getScm() == null )
{
throw new MavenReportException( "SCM Connection is not set." );
}
String scmConnection = project.getScm().getConnection();
if ( StringUtils.isNotEmpty( scmConnection ) && "connection".equals( connectionType.toLowerCase() ) )
{
connection = scmConnection;
}
String scmDeveloper = project.getScm().getDeveloperConnection();
if ( StringUtils.isNotEmpty( scmDeveloper ) && "developerconnection".equals( connectionType.toLowerCase() ) )
{
connection = scmDeveloper;
}
if ( StringUtils.isEmpty( connection ) )
{
throw new MavenReportException( "SCM Connection is not set." );
}
return connection;
}
/**
* checks whether there are enough configuration parameters to generate the report
*
* @throws MavenReportException when there is insufficient paramters to generate the report
*/
private void verifySCMTypeParams()
throws MavenReportException
{
if ( "range".equals( type ) )
{
if ( range == -1 )
{
range = DEFAULT_RANGE;
}
}
else if ( "date".equals( type ) )
{
if ( dates == null )
{
throw new MavenReportException(
"The dates parameter is required when type=\"date\"."
+ " The value should be the absolute date for the start of the log." );
}
}
else if ( "tag".equals( type ) )
{
if ( tags == null )
{
throw new MavenReportException( "The tags parameter is required when type=\"tag\"." );
}
}
else
{
throw new MavenReportException( "The type parameter has an invalid value: " + type
+ ". The value should be \"range\", \"date\", or \"tag\"." );
}
}
/**
* generates an empty report in case there are no sources to generate a report with
*
* @param bundle the resource bundle to retrieve report phrases from
* @param sink the report formatting tool
*/
protected void doGenerateEmptyReport( ResourceBundle bundle, Sink sink )
{
sink.head();
sink.title();
sink.text( bundle.getString( "report.changelog.header" ) );
sink.title_();
sink.head_();
sink.body();
sink.section1();
sink.sectionTitle1();
sink.text( bundle.getString( "report.changelog.mainTitle" ) );
sink.sectionTitle1_();
sink.paragraph();
sink.text( bundle.getString( "report.changelog.nosources" ) );
sink.paragraph_();
sink.section1_();
sink.body_();
sink.flush();
sink.close();
}
/**
* method that generates the report for this mojo. This method is overridden by dev-activity and file-activity mojo
*
* @param changeLogSets changed sets to generate the report from
* @param bundle the resource bundle to retrieve report phrases from
* @param sink the report formatting tool
*/
protected void doGenerateReport( List<ChangeLogSet> changeLogSets, ResourceBundle bundle, Sink sink )
{
sink.head();
sink.title();
sink.text( bundle.getString( "report.changelog.header" ) );
sink.title_();
sink.head_();
sink.body();
sink.section1();
sink.sectionTitle1();
sink.text( bundle.getString( "report.changelog.mainTitle" ) );
sink.sectionTitle1_();
// Summary section
doSummarySection( changeLogSets, bundle, sink );
for ( ChangeLogSet changeLogSet : changeLogSets )
{
doChangedSet( changeLogSet, bundle, sink );
}
sink.section1_();
sink.body_();
sink.flush();
sink.close();
}
/**
* generates the report summary section of the report
*
* @param changeLogSets changed sets to generate the report from
* @param bundle the resource bundle to retrieve report phrases from
* @param sink the report formatting tool
*/
private void doSummarySection( List<ChangeLogSet> changeLogSets, ResourceBundle bundle, Sink sink )
{
sink.paragraph();
sink.text( bundle.getString( "report.changelog.ChangedSetsTotal" ) );
sink.text( ": " + changeLogSets.size() );
sink.paragraph_();
}
/**
* generates a section of the report referring to a changeset
*
* @param set the current ChangeSet to generate this section of the report
* @param bundle the resource bundle to retrieve report phrases from
* @param sink the report formatting tool
*/
private void doChangedSet( ChangeLogSet set, ResourceBundle bundle, Sink sink )
{
sink.section2();
doChangeSetTitle( set, bundle, sink );
doSummary( set, bundle, sink );
doChangedSetTable( set.getChangeSets(), bundle, sink );
sink.section2_();
}
/**
* Generate the title for the report.
*
* @param set change set to generate the report from
* @param bundle the resource bundle to retrieve report phrases from
* @param sink the report formatting tool
*/
protected void doChangeSetTitle( ChangeLogSet set, ResourceBundle bundle, Sink sink )
{
sink.sectionTitle2();
SimpleDateFormat headingDateFormater = new SimpleDateFormat( headingDateFormat );
if ( "tag".equals( type ) )
{
if ( set.getStartVersion() == null || set.getStartVersion().getName() == null )
{
sink.text( bundle.getString( "report.SetTagCreation" ) );
if ( set.getEndVersion() != null && set.getEndVersion().getName() != null )
{
sink.text( " " + bundle.getString( "report.SetTagUntil" ) + " '" + set.getEndVersion() + "'" );
}
}
else if ( set.getEndVersion() == null || set.getEndVersion().getName() == null )
{
sink.text( bundle.getString( "report.SetTagSince" ) );
sink.text( " '" + set.getStartVersion() + "'" );
}
else
{
sink.text( bundle.getString( "report.SetTagBetween" ) );
sink.text( " '" + set.getStartVersion() + "' " + bundle.getString( "report.And" ) + " '"
+ set.getEndVersion() + "'" );
}
}
else if ( set.getStartDate() == null )
{
sink.text( bundle.getString( "report.SetRangeUnknown" ) );
}
else if ( set.getEndDate() == null )
{
sink.text( bundle.getString( "report.SetRangeSince" ) );
sink.text( " " + headingDateFormater.format( set.getStartDate() ) );
}
else
{
sink.text( bundle.getString( "report.SetRangeBetween" ) );
sink.text( " " + headingDateFormater.format( set.getStartDate() )
+ " " + bundle.getString( "report.And" ) + " "
+ headingDateFormater.format( set.getEndDate() ) );
}
sink.sectionTitle2_();
}
/**
* Generate the summary section of the report.
*
* @param set change set to generate the report from
* @param bundle the resource bundle to retrieve report phrases from
* @param sink the report formatting tool
*/
protected void doSummary( ChangeLogSet set, ResourceBundle bundle, Sink sink )
{
sink.paragraph();
sink.text( bundle.getString( "report.TotalCommits" ) );
sink.text( ": " + set.getChangeSets().size() );
sink.lineBreak();
sink.text( bundle.getString( "report.changelog.FilesChanged" ) );
sink.text( ": " + countFilesChanged( set.getChangeSets() ) );
sink.paragraph_();
}
/**
* counts the number of files that were changed in the specified SCM
*
* @param entries a collection of SCM changes
* @return number of files changed for the changedsets
*/
protected long countFilesChanged( Collection<ChangeSet> entries )
{
if ( entries == null )
{
return 0;
}
if ( entries.isEmpty() )
{
return 0;
}
HashMap<String, List<ChangeFile>> fileList = new HashMap<String, List<ChangeFile>>();
for ( ChangeSet entry : entries )
{
for ( ChangeFile file : entry.getFiles() )
{
String name = file.getName();
List<ChangeFile> list = fileList.get( name );
if ( list != null )
{
list.add( file );
}
else
{
list = new LinkedList<ChangeFile>();
list.add( file );
fileList.put( name, list );
}
}
}
return fileList.size();
}
/**
* generates the report table showing the SCM log entries
*
* @param entries a list of change log entries to generate the report from
* @param bundle the resource bundle to retrieve report phrases from
* @param sink the report formatting tool
*/
private void doChangedSetTable( Collection<ChangeSet> entries, ResourceBundle bundle, Sink sink )
{
sink.table();
sink.tableRow();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.changelog.timestamp" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.changelog.author" ) );
sink.tableHeaderCell_();
sink.tableHeaderCell();
sink.text( bundle.getString( "report.changelog.details" ) );
sink.tableHeaderCell_();
sink.tableRow_();
initReportUrls();
List<ChangeSet> sortedEntries = new ArrayList<ChangeSet>( entries );
Collections.sort( sortedEntries, new Comparator<ChangeSet>()
{
public int compare( ChangeSet changeSet0, ChangeSet changeSet1 )
{
return changeSet1.getDate().compareTo( changeSet0.getDate() );
}
} );
for ( ChangeSet entry : sortedEntries )
{
doChangedSetDetail( entry, bundle, sink );
}
sink.table_();
}
/**
* reports on the details of an SCM entry log
*
* @param entry an SCM entry to generate the report from
* @param bundle the resource bundle to retrieve report phrases from
* @param sink the report formatting tool
*/
private void doChangedSetDetail( ChangeSet entry, ResourceBundle bundle, Sink sink )
{
sink.tableRow();
sink.tableCell();
sink.text( entry.getDateFormatted() + " " + entry.getTimeFormatted() );
sink.tableCell_();
sink.tableCell();
sinkAuthorDetails( sink, entry.getAuthor() );
sink.tableCell_();
sink.tableCell();
//doRevision( entry.getFiles(), bundle, sink );
doChangedFiles( entry.getFiles(), sink );
sink.lineBreak();
StringReader sr = new StringReader( entry.getComment() );
BufferedReader br = new BufferedReader( sr );
String line;
try
{
if ( ( issueIDRegexPattern != null && issueIDRegexPattern.length() > 0 )
&& ( issueLinkUrl != null && issueLinkUrl.length() > 0 ) )
{
Pattern pattern = Pattern.compile( issueIDRegexPattern );
line = br.readLine();
while ( line != null )
{
sinkIssueLink( sink, line, pattern );
line = br.readLine();
if ( line != null )
{
sink.lineBreak();
}
}
}
else
{
line = br.readLine();
while ( line != null )
{
sink.text( line );
line = br.readLine();
if ( line != null )
{
sink.lineBreak();
}
}
}
}
catch ( IOException e )
{
getLog().warn( "Unable to read the comment of a ChangeSet as a stream." );
}
finally
{
if ( br != null )
{
try
{
br.close();
}
catch ( IOException e )
{
getLog().warn( "Unable to close a reader." );
}
}
if ( sr != null )
{
sr.close();
}
}
sink.tableCell_();
sink.tableRow_();
}
private void sinkIssueLink( Sink sink, String line, Pattern pattern )
{
// replace any ticket patterns found.
Matcher matcher = pattern.matcher( line );
int currLoc = 0;
while ( matcher.find() )
{
String match = matcher.group();
String link;
if ( issueLinkUrl.indexOf( ISSUE_TOKEN ) > 0 )
{
link = issueLinkUrl.replaceAll( ISSUE_TOKEN, match );
}
else
{
if ( issueLinkUrl.endsWith( "/" ) )
{
link = issueLinkUrl;
}
else
{
link = issueLinkUrl + "/";
}
link += match;
}
int startOfMatch = matcher.start();
String unmatchedText = line.substring( currLoc, startOfMatch );
currLoc = matcher.end();
sink.text( unmatchedText );
sink.link( link );
sink.text( match );
sink.link_();
}
sink.text( line.substring( currLoc ) );
}
/**
* If the supplied author is a known developer this method outputs a
* link to the team members report, or alternatively, if the supplied
* author is unknown, outputs the author's name as plain text.
*
* @param sink Sink to use for outputting
* @param author The author's name.
*/
protected void sinkAuthorDetails( Sink sink, String author )
{
Developer developer = (Developer) developersById.get( author );
if ( developer == null )
{
developer = (Developer) developersByName.get( author );
}
if ( developer != null )
{
sink.link( "team-list.html#" + developer.getId() );
sink.text( developer.getName() );
sink.link_();
}
else
{
sink.text( author );
}
}
/**
* populates the report url used to create links from certain elements of the report
*/
protected void initReportUrls()
{
if ( scmUrl != null )
{
int idx = scmUrl.indexOf( '?' );
if ( idx > 0 )
{
rptRepository = scmUrl.substring( 0, idx );
if ( scmUrl.equals( displayFileDetailUrl ) )
{
String rptTmpMultiRepoParam = scmUrl.substring( rptRepository.length() );
rptOneRepoParam = "?" + rptTmpMultiRepoParam.substring( 1 );
rptMultiRepoParam = "&" + rptTmpMultiRepoParam.substring( 1 );
}
}
else
{
rptRepository = scmUrl;
rptOneRepoParam = "";
rptMultiRepoParam = "";
}
}
}
/**
* generates the section of the report listing all the files revisions
*
* @param files list of files to generate the reports from
* @param sink the report formatting tool
*/
private void doChangedFiles( List<ChangeFile> files, Sink sink )
{
for ( ChangeFile file : files )
{
sinkLogFile( sink, file.getName(), file.getRevision() );
}
}
/**
* generates the section of the report detailing the revisions made and the files changed
*
* @param sink the report formatting tool
* @param name filename of the changed file
* @param revision the revision code for this file
*/
private void sinkLogFile( Sink sink, String name, String revision )
{
try
{
String connection = getConnection();
generateLinks( connection, name, revision, sink );
}
catch ( Exception e )
{
getLog().debug( e );
sink.text( name + " v " + revision );
}
sink.lineBreak();
}
/**
* attaches the http links from the changed files
*
* @param connection the string used to connect to the SCM
* @param name filename of the file that was changed
* @param sink the report formatting tool
*/
protected void generateLinks( String connection, String name, Sink sink )
{
generateLinks( connection, name, null, sink );
}
/**
* attaches the http links from the changed files
*
* @param connection the string used to connect to the SCM
* @param name filename of the file that was changed
* @param revision the revision code
* @param sink the report formatting tool
*/
protected void generateLinks( String connection, String name, String revision, Sink sink )
{
String linkFile = null;
String linkRev = null;
if ( revision != null )
{
linkFile = displayFileRevDetailUrl;
}
else
{
linkFile = displayFileDetailUrl;
}
if ( linkFile != null )
{
if ( !linkFile.equals( scmUrl ) )
{
String linkName = name;
if(encodeFileUri)
{
try
{
linkName = URLEncoder.encode(linkName, "UTF-8");
}
catch (UnsupportedEncodingException e)
{
// UTF-8 is always supported
}
}
// Use the given URL to create links to the files
if ( linkFile.indexOf( FILE_TOKEN ) > 0 )
{
linkFile = linkFile.replaceAll( FILE_TOKEN, linkName );
}
else
{
// This is here so that we are backwards compatible with the
// format used before the special token was introduced
linkFile = linkFile + linkName;
}
// Use the given URL to create links to the files
if ( revision != null && linkFile.indexOf( REV_TOKEN ) > 0 )
{
linkFile = linkFile.replaceAll( REV_TOKEN, revision );
}
}
else if ( connection.startsWith( "scm:perforce" ) )
{
String path = getAbsolutePath( displayFileDetailUrl, name );
linkFile = path + "?ac=22";
if ( revision != null )
{
linkRev = path + "?ac=64&rev=" + revision;
}
}
else if ( connection.startsWith( "scm:clearcase" ) )
{
String path = getAbsolutePath( displayFileDetailUrl, name );
linkFile = path + rptOneRepoParam;
}
else if ( connection.indexOf( "cvsmonitor.pl" ) > 0 )
{
String module = rptOneRepoParam.replaceAll( "^.*(&amp;module=.*?(?:&amp;|$)).*$", "$1" );
linkFile = displayFileDetailUrl + "?cmd=viewBrowseFile" + module + "&file=" + name;
if ( revision != null )
{
linkRev =
rptRepository + "?cmd=viewBrowseVersion" + module + "&file=" + name + "&version=" + revision;
}
}
else
{
String path = getAbsolutePath( displayFileDetailUrl, name );
linkFile = path + rptOneRepoParam;
if ( revision != null )
{
linkRev = path + "?rev=" + revision + "&content-type=text/vnd.viewcvs-markup" + rptMultiRepoParam;
}
}
}
if ( linkFile != null )
{
sink.link( linkFile );
sinkFileName( name, sink );
sink.link_();
}
else
{
sinkFileName( name, sink );
}
sink.text( " " );
if ( linkRev == null && revision != null && displayChangeSetDetailUrl != null )
{
if ( displayChangeSetDetailUrl.indexOf( REV_TOKEN ) > 0 )
{
linkRev = displayChangeSetDetailUrl.replaceAll( REV_TOKEN, revision );
}
else
{
linkRev = displayChangeSetDetailUrl + revision;
}
}
if ( linkRev != null )
{
sink.link( linkRev );
sink.text( "v " + revision );
sink.link_();
}
else if ( revision != null )
{
sink.text( "v " + revision );
}
}
/**
* Encapsulates the logic for rendering the name with a bolded markup.
*
* @param name filename of the file that was changed
* @param sink the report formatting tool
*/
private void sinkFileName( String name, Sink sink )
{
name = name.replaceAll( "\\\\", "/" );
int pos = name.lastIndexOf( '/' );
String head;
String tail;
if ( pos < 0 )
{
head = "";
tail = name;
}
else
{
head = name.substring( 0, pos ) + "/";
tail = name.substring( pos + 1 );
}
sink.text( head );
sink.bold();
sink.text( tail );
sink.bold_();
}
/**
* calculates the path from a base directory to a target file
*
* @param base base directory to create the absolute path from
* @param target target file to create the absolute path to
*/
private String getAbsolutePath( final String base, final String target )
{
String absPath = "";
StringTokenizer baseTokens = new StringTokenizer( base.replaceAll( "\\\\", "/" ), "/", true );
StringTokenizer targetTokens = new StringTokenizer( target.replaceAll( "\\\\", "/" ), "/" );
String targetRoot = targetTokens.nextToken();
while ( baseTokens.hasMoreTokens() )
{
String baseToken = baseTokens.nextToken();
if ( baseToken.equals( targetRoot ) )
{
break;
}
absPath += baseToken;
}
if ( !absPath.endsWith( "/" ) )
{
absPath += "/";
}
String newTarget = target;
if ( newTarget.startsWith( "/" ) )
{
newTarget = newTarget.substring( 1 );
}
return absPath + newTarget;
}
/** {@inheritDoc} */
protected MavenProject getProject()
{
return project;
}
/** {@inheritDoc} */
protected String getOutputDirectory()
{
if ( !outputDirectory.isAbsolute() )
{
outputDirectory = new File( project.getBasedir(), outputDirectory.getPath() );
}
return outputDirectory.getAbsolutePath();
}
/**
* Gets the effective reporting output files encoding.
*
* @return The effective reporting output file encoding, never <code>null</code>.
*/
protected String getOutputEncoding()
{
return ( outputEncoding != null ) ? outputEncoding : ReaderFactory.UTF_8;
}
/** {@inheritDoc} */
protected Renderer getSiteRenderer()
{
return siteRenderer;
}
/** {@inheritDoc} */
public String getDescription( Locale locale )
{
return getBundle( locale ).getString( "report.changelog.description" );
}
/** {@inheritDoc} */
public String getName( Locale locale )
{
return getBundle( locale ).getString( "report.changelog.name" );
}
/** {@inheritDoc} */
public String getOutputName()
{
return "changelog";
}
/**
* @param locale
* @return the current bundle
*/
protected ResourceBundle getBundle( Locale locale )
{
return ResourceBundle.getBundle( "scm-activity", locale, this.getClass().getClassLoader() );
}
/** {@inheritDoc} */
public boolean canGenerateReport()
{
if ( offline && !outputXML.exists() )
{
return false;
}
if ( skip )
{
return false;
}
return true;
}
}