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