1 package org.apache.maven.plugin.changes;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import java.io.File;
23 import java.io.FileWriter;
24 import java.io.IOException;
25 import java.io.Writer;
26
27 import java.net.URL;
28
29 import java.text.SimpleDateFormat;
30
31 import java.util.Collections;
32 import java.util.Date;
33 import java.util.List;
34 import java.util.Locale;
35 import java.util.Map;
36 import java.util.Properties;
37 import java.util.ResourceBundle;
38
39 import org.apache.commons.collections.map.CaseInsensitiveMap;
40 import org.apache.maven.execution.MavenSession;
41 import org.apache.maven.plugins.annotations.Component;
42 import org.apache.maven.plugins.annotations.Mojo;
43 import org.apache.maven.plugins.annotations.Parameter;
44 import org.apache.maven.project.MavenProject;
45 import org.apache.maven.reporting.MavenReportException;
46 import org.apache.maven.shared.filtering.MavenFileFilter;
47 import org.apache.maven.shared.filtering.MavenFileFilterRequest;
48 import org.apache.maven.shared.filtering.MavenFilteringException;
49
50 import org.codehaus.plexus.util.FileUtils;
51 import org.codehaus.plexus.util.IOUtil;
52 import org.codehaus.plexus.util.StringUtils;
53 import org.apache.commons.io.input.XmlStreamReader;
54
55
56
57
58
59
60
61 @Mojo( name = "changes-report", threadSafe = true )
62 public class ChangesMojo
63 extends AbstractChangesReport
64 {
65
66
67
68
69
70
71 @Parameter( defaultValue = "false" )
72 private boolean aggregated;
73
74
75
76
77
78
79
80 @Parameter( property = "changes.addActionDate", defaultValue = "false" )
81 private boolean addActionDate;
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100 @Parameter( defaultValue = "true" )
101 private boolean escapeHTML;
102
103
104
105
106
107
108 @Parameter( defaultValue = "${project.build.directory}/changes", required = true, readonly = true )
109 private File filteredOutputDirectory;
110
111
112
113
114
115
116 @Parameter( defaultValue = "false" )
117 private boolean filteringChanges;
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132 @Parameter( property = "changes.issueLinkTemplate", defaultValue = "%URL%/ViewIssue.jspa?key=%ISSUE%" )
133 private String issueLinkTemplate;
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156 @Parameter
157 private Map issueLinkTemplatePerSystem;
158
159
160
161
162 @Component
163 private MavenFileFilter mavenFileFilter;
164
165
166
167
168
169
170
171 @Parameter( defaultValue = "yyyy-MM-dd" )
172 private String publishDateFormat;
173
174
175
176
177
178
179
180 @Parameter( defaultValue = "en" )
181 private String publishDateLocale;
182
183
184
185
186 @Parameter( defaultValue = "${session}", readonly = true, required = true )
187 protected MavenSession session;
188
189
190
191
192 @Parameter( defaultValue = "${project.issueManagement.system}", readonly = true )
193 private String system;
194
195
196
197
198
199
200
201 @Parameter( defaultValue = "team-list.html" )
202 private String teamlist;
203
204
205
206 @Parameter( defaultValue = "${project.issueManagement.url}", readonly = true )
207 private String url;
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222 @Parameter
223 private String feedType;
224
225
226
227
228 @Parameter( property = "changes.xmlPath", defaultValue = "src/changes/changes.xml" )
229 private File xmlPath;
230
231 private ReleaseUtils releaseUtils = new ReleaseUtils( getLog() );
232
233 private CaseInsensitiveMap caseInsensitiveIssueLinkTemplatePerSystem;
234
235
236
237
238
239 public boolean canGenerateReport()
240 {
241
242 if ( runOnlyAtExecutionRoot && !isThisTheExecutionRoot() )
243 {
244 getLog().info( "Skipping the Changes Report in this project because it's not the Execution Root" );
245 return false;
246 }
247 return xmlPath.isFile();
248 }
249
250 public void executeReport( Locale locale )
251 throws MavenReportException
252 {
253 Date now = new Date();
254 SimpleDateFormat simpleDateFormat =
255 new SimpleDateFormat( publishDateFormat, new Locale( publishDateLocale ) );
256 Properties additionalProperties = new Properties();
257 additionalProperties.put( "publishDate", simpleDateFormat.format( now ) );
258
259 ChangesXML changesXml = getChangesFromFile( xmlPath, project, additionalProperties );
260 if ( changesXml == null )
261 {
262 return;
263 }
264
265 if ( aggregated )
266 {
267 final String basePath = project.getBasedir().getAbsolutePath();
268 final String absolutePath = xmlPath.getAbsolutePath();
269 if ( !absolutePath.startsWith( basePath ) )
270 {
271 getLog().warn( "xmlPath should be within the project dir for aggregated changes report." );
272 return;
273 }
274 final String relativePath = absolutePath.substring( basePath.length() );
275
276 List releaseList = changesXml.getReleaseList();
277 for ( Object o : project.getCollectedProjects() )
278 {
279 final MavenProject childProject = (MavenProject) o;
280 final File changesFile = new File( childProject.getBasedir(), relativePath );
281 final ChangesXML childXml = getChangesFromFile( changesFile, childProject, additionalProperties );
282 if ( childXml != null )
283 {
284 releaseList = releaseUtils.mergeReleases( releaseList, childProject.getName(),
285 childXml.getReleaseList() );
286 }
287 }
288 changesXml.setReleaseList( releaseList );
289 }
290
291 ChangesReportGenerator report = new ChangesReportGenerator( changesXml.getReleaseList() );
292
293 report.setAuthor( changesXml.getAuthor() );
294 report.setTitle( changesXml.getTitle() );
295
296 report.setEscapeHTML ( escapeHTML );
297
298
299
300 if ( issueLinkTemplatePerSystem == null )
301 {
302 caseInsensitiveIssueLinkTemplatePerSystem = new CaseInsensitiveMap();
303 }
304 else
305 {
306 caseInsensitiveIssueLinkTemplatePerSystem = new CaseInsensitiveMap( issueLinkTemplatePerSystem );
307 }
308
309
310
311 addIssueLinkTemplate( ChangesReportGenerator.DEFAULT_ISSUE_SYSTEM_KEY, issueLinkTemplate );
312 addIssueLinkTemplate( "Bitbucket", "%URL%/issue/%ISSUE%" );
313 addIssueLinkTemplate( "Bugzilla", "%URL%/show_bug.cgi?id=%ISSUE%" );
314 addIssueLinkTemplate( "GitHub", "%URL%/%ISSUE%" );
315 addIssueLinkTemplate( "GoogleCode", "%URL%/detail?id=%ISSUE%" );
316 addIssueLinkTemplate( "JIRA", "%URL%/%ISSUE%" );
317 addIssueLinkTemplate( "Mantis", "%URL%/view.php?id=%ISSUE%" );
318 addIssueLinkTemplate( "MKS", "%URL%/viewissue?selection=%ISSUE%" );
319 addIssueLinkTemplate( "Redmine", "%URL%/issues/show/%ISSUE%" );
320 addIssueLinkTemplate( "Scarab", "%URL%/issues/id/%ISSUE%" );
321 addIssueLinkTemplate( "SourceForge", "http://sourceforge.net/support/tracker.php?aid=%ISSUE%" );
322 addIssueLinkTemplate( "SourceForge2", "%URL%/%ISSUE%" );
323 addIssueLinkTemplate( "Trac", "%URL%/ticket/%ISSUE%" );
324 addIssueLinkTemplate( "Trackplus", "%URL%/printItem.action?key=%ISSUE%" );
325 addIssueLinkTemplate( "YouTrack", "%URL%/issue/%ISSUE%" );
326
327
328
329
330 logIssueLinkTemplatePerSystem( caseInsensitiveIssueLinkTemplatePerSystem );
331
332 report.setIssueLinksPerSystem( caseInsensitiveIssueLinkTemplatePerSystem );
333
334 report.setSystem( system );
335
336 report.setTeamlist ( teamlist );
337
338 report.setUrl( url );
339
340 report.setAddActionDate( addActionDate );
341
342 if ( StringUtils.isEmpty( url ) )
343 {
344 getLog().warn( "No issue management URL defined in POM. Links to your issues will not work correctly." );
345 }
346
347 boolean feedGenerated = false;
348
349 if ( StringUtils.isNotEmpty( feedType ) )
350 {
351 feedGenerated = generateFeed( changesXml, locale );
352 }
353
354 report.setLinkToFeed( feedGenerated );
355
356 report.doGenerateReport( getBundle( locale ), getSink() );
357
358
359 copyStaticResources();
360 }
361
362 public String getDescription( Locale locale )
363 {
364 return getBundle( locale ).getString( "report.issues.description" );
365 }
366
367 public String getName( Locale locale )
368 {
369 return getBundle( locale ).getString( "report.issues.name" );
370 }
371
372 public String getOutputName()
373 {
374 return "changes-report";
375 }
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391 private ChangesXML getChangesFromFile( File changesXml, MavenProject project, Properties additionalProperties )
392 throws MavenReportException
393 {
394 if ( !changesXml.exists() )
395 {
396 getLog().warn( "changes.xml file " + changesXml.getAbsolutePath() + " does not exist." );
397 return null;
398 }
399
400 if ( filteringChanges )
401 {
402 if ( !filteredOutputDirectory.exists() )
403 {
404 filteredOutputDirectory.mkdirs();
405 }
406 XmlStreamReader xmlStreamReader = null;
407 try
408 {
409
410 xmlStreamReader = new XmlStreamReader( changesXml );
411 String encoding = xmlStreamReader.getEncoding();
412 File resultFile = new File( filteredOutputDirectory,
413 project.getGroupId() + "." + project.getArtifactId() + "-changes.xml" );
414
415 final MavenFileFilterRequest mavenFileFilterRequest =
416 new MavenFileFilterRequest( changesXml, resultFile, true, project, Collections.EMPTY_LIST,
417 false, encoding, session, additionalProperties );
418 mavenFileFilter.copyFile( mavenFileFilterRequest );
419 changesXml = resultFile;
420 }
421 catch ( IOException e )
422 {
423 throw new MavenReportException( "Exception during filtering changes file : " + e.getMessage(), e );
424 }
425 catch ( MavenFilteringException e )
426 {
427 throw new MavenReportException( "Exception during filtering changes file : " + e.getMessage(), e );
428 }
429 finally
430 {
431 if ( xmlStreamReader != null )
432 {
433 IOUtil.close( xmlStreamReader );
434 }
435 }
436
437 }
438 return new ChangesXML( changesXml, getLog() );
439 }
440
441
442
443
444
445
446
447
448
449 private void addIssueLinkTemplate( String system, String issueLinkTemplate )
450 {
451 if ( caseInsensitiveIssueLinkTemplatePerSystem == null )
452 {
453 caseInsensitiveIssueLinkTemplatePerSystem = new CaseInsensitiveMap();
454 }
455 if ( !caseInsensitiveIssueLinkTemplatePerSystem.containsKey( system ) )
456 {
457 caseInsensitiveIssueLinkTemplatePerSystem.put( system, issueLinkTemplate );
458 }
459 }
460
461 private void copyStaticResources()
462 throws MavenReportException
463 {
464 final String pluginResourcesBase = "org/apache/maven/plugin/changes";
465 String resourceNames[] = {
466 "images/add.gif",
467 "images/fix.gif",
468 "images/icon_help_sml.gif",
469 "images/remove.gif",
470 "images/rss.png",
471 "images/update.gif" };
472 try
473 {
474 getLog().debug( "Copying static resources." );
475 for ( String resourceName : resourceNames )
476 {
477 URL url = this.getClass().getClassLoader().getResource( pluginResourcesBase + "/" + resourceName );
478 FileUtils.copyURLToFile( url, new File( getReportOutputDirectory(), resourceName ) );
479 }
480 }
481 catch ( IOException e )
482 {
483 throw new MavenReportException( "Unable to copy static resources." );
484 }
485 }
486
487 private ResourceBundle getBundle( Locale locale )
488 {
489 return ResourceBundle.getBundle( "changes-report", locale, this.getClass().getClassLoader() );
490 }
491
492 protected String getTeamlist()
493 {
494 return teamlist;
495 }
496
497 private void logIssueLinkTemplatePerSystem( Map issueLinkTemplatePerSystem )
498 {
499 if ( getLog().isDebugEnabled() )
500 {
501 if ( issueLinkTemplatePerSystem == null )
502 {
503 getLog().debug( "No issueLinkTemplatePerSystem configuration was found" );
504 }
505 else
506 {
507 for ( Object o : issueLinkTemplatePerSystem.entrySet() )
508 {
509 Map.Entry entry = (Map.Entry) o;
510 getLog().debug( "issueLinkTemplatePerSystem[" + entry.getKey() + "] = " + entry.getValue() );
511 }
512 }
513 }
514 }
515
516 private boolean generateFeed( final ChangesXML changesXml, final Locale locale )
517 {
518 getLog().debug( "Generating " + feedType + " feed." );
519
520 boolean success = true;
521
522 final FeedGenerator feed = new FeedGenerator( locale );
523 feed.setLink( project.getUrl() + "/changes-report.html" );
524 feed.setTitle( project.getName() + ": " + changesXml.getTitle() );
525 feed.setAuthor( changesXml.getAuthor() );
526 feed.setDateFormat( new SimpleDateFormat( publishDateFormat, new Locale( publishDateLocale ) ) );
527
528 Writer writer = null;
529
530 try
531 {
532 writer = new FileWriter( new File( getReportOutputDirectory(), "changes.rss" ) );
533 feed.export( changesXml.getReleaseList(), feedType, writer );
534 }
535 catch ( IOException ex )
536 {
537 success = false;
538 getLog().warn( "Failed to create rss feed: " + ex.getMessage() );
539 getLog().debug( ex );
540 }
541 finally
542 {
543 try
544 {
545 if ( writer != null )
546 {
547 writer.close();
548 }
549 }
550 catch ( IOException ex )
551 {
552 getLog().warn( "Failed to close writer: " + ex.getMessage() );
553 getLog().debug( ex );
554 }
555 }
556
557 return success;
558 }
559 }