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