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