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 import java.net.URL;
27 import java.text.SimpleDateFormat;
28 import java.util.Collections;
29 import java.util.Date;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.Map;
33 import java.util.Map.Entry;
34 import java.util.Properties;
35 import java.util.ResourceBundle;
36
37 import org.apache.commons.collections.map.CaseInsensitiveMap;
38 import org.apache.commons.io.input.XmlStreamReader;
39 import org.apache.maven.execution.MavenSession;
40 import org.apache.maven.plugins.annotations.Component;
41 import org.apache.maven.plugins.annotations.Mojo;
42 import org.apache.maven.plugins.annotations.Parameter;
43 import org.apache.maven.plugins.changes.model.Release;
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 import org.codehaus.plexus.util.FileUtils;
50 import org.codehaus.plexus.util.IOUtil;
51 import org.codehaus.plexus.util.StringUtils;
52
53
54
55
56
57
58
59 @Mojo( name = "changes-report", threadSafe = true )
60 public class ChangesMojo
61 extends AbstractChangesReport
62 {
63
64
65
66
67
68
69 @Parameter( defaultValue = "false" )
70 private boolean aggregated;
71
72
73
74
75
76
77
78 @Parameter( property = "changes.addActionDate", defaultValue = "false" )
79 private boolean addActionDate;
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95 @Parameter( defaultValue = "true" )
96 private boolean escapeHTML;
97
98
99
100
101
102
103 @Parameter( defaultValue = "${project.build.directory}/changes", required = true, readonly = true )
104 private File filteredOutputDirectory;
105
106
107
108
109
110
111 @Parameter( defaultValue = "false" )
112 private boolean filteringChanges;
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127 @Parameter( property = "changes.issueLinkTemplate", defaultValue = "%URL%/ViewIssue.jspa?key=%ISSUE%" )
128 private String issueLinkTemplate;
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149 @Parameter
150 private Map<String, String> issueLinkTemplatePerSystem;
151
152
153
154
155 @Component
156 private MavenFileFilter mavenFileFilter;
157
158
159
160
161
162
163
164 @Parameter( defaultValue = "yyyy-MM-dd" )
165 private String publishDateFormat;
166
167
168
169
170
171
172
173 @Parameter( defaultValue = "en" )
174 private String publishDateLocale;
175
176
177
178
179 @Parameter( defaultValue = "${session}", readonly = true, required = true )
180 protected MavenSession session;
181
182
183
184
185 @Parameter( defaultValue = "${project.issueManagement.system}", readonly = true )
186 private String system;
187
188
189
190
191
192
193
194 @Parameter( defaultValue = "team-list.html" )
195 private String teamlist;
196
197
198
199 @Parameter( defaultValue = "${project.issueManagement.url}", readonly = true )
200 private String url;
201
202
203
204
205
206
207
208
209
210
211
212
213
214 @Parameter
215 private String feedType;
216
217
218
219
220 @Parameter( property = "changes.xmlPath", defaultValue = "src/changes/changes.xml" )
221 private File xmlPath;
222
223 private ReleaseUtils releaseUtils = new ReleaseUtils( getLog() );
224
225 private CaseInsensitiveMap caseInsensitiveIssueLinkTemplatePerSystem;
226
227
228
229
230
231 public boolean canGenerateReport()
232 {
233
234 if ( runOnlyAtExecutionRoot && !isThisTheExecutionRoot() )
235 {
236 getLog().info( "Skipping the Changes Report in this project because it's not the Execution Root" );
237 return false;
238 }
239 return xmlPath.isFile();
240 }
241
242 public void executeReport( Locale locale )
243 throws MavenReportException
244 {
245 Date now = new Date();
246 SimpleDateFormat simpleDateFormat = new SimpleDateFormat( publishDateFormat, new Locale( publishDateLocale ) );
247 Properties additionalProperties = new Properties();
248 additionalProperties.put( "publishDate", simpleDateFormat.format( now ) );
249
250 ChangesXML changesXml = getChangesFromFile( xmlPath, project, additionalProperties );
251 if ( changesXml == null )
252 {
253 return;
254 }
255
256 if ( aggregated )
257 {
258 final String basePath = project.getBasedir().getAbsolutePath();
259 final String absolutePath = xmlPath.getAbsolutePath();
260 if ( !absolutePath.startsWith( basePath ) )
261 {
262 getLog().warn( "xmlPath should be within the project dir for aggregated changes report." );
263 return;
264 }
265 final String relativePath = absolutePath.substring( basePath.length() );
266
267 List<Release> releaseList = changesXml.getReleaseList();
268 for ( Object o : project.getCollectedProjects() )
269 {
270 final MavenProject childProject = (MavenProject) o;
271 final File changesFile = new File( childProject.getBasedir(), relativePath );
272 final ChangesXML childXml = getChangesFromFile( changesFile, childProject, additionalProperties );
273 if ( childXml != null )
274 {
275 releaseList =
276 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,
404 project.getGroupId() + "." + project.getArtifactId() + "-changes.xml" );
405
406 final MavenFileFilterRequest mavenFileFilterRequest =
407 new MavenFileFilterRequest( changesXml, resultFile, true, project, Collections.<String>emptyList(),
408 false, encoding, session, additionalProperties );
409 mavenFileFilter.copyFile( mavenFileFilterRequest );
410 changesXml = resultFile;
411 xmlStreamReader.close();
412 xmlStreamReader = null;
413 }
414 catch ( IOException e )
415 {
416 throw new MavenReportException( "Exception during filtering changes file : " + e.getMessage(), e );
417 }
418 catch ( MavenFilteringException e )
419 {
420 throw new MavenReportException( "Exception during filtering changes file : " + e.getMessage(), e );
421 }
422 finally
423 {
424 IOUtil.close( xmlStreamReader );
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[] = { "images/add.gif", "images/fix.gif", "images/icon_help_sml.gif", "images/remove.gif",
456 "images/rss.png", "images/update.gif" };
457 try
458 {
459 getLog().debug( "Copying static resources." );
460 for ( String resourceName : resourceNames )
461 {
462 URL url = this.getClass().getClassLoader().getResource( pluginResourcesBase + "/" + resourceName );
463 FileUtils.copyURLToFile( url, new File( getReportOutputDirectory(), resourceName ) );
464 }
465 }
466 catch ( IOException e )
467 {
468 throw new MavenReportException( "Unable to copy static resources." );
469 }
470 }
471
472 private ResourceBundle getBundle( Locale locale )
473 {
474 return ResourceBundle.getBundle( "changes-report", locale, this.getClass().getClassLoader() );
475 }
476
477 protected String getTeamlist()
478 {
479 return teamlist;
480 }
481
482 private void logIssueLinkTemplatePerSystem( Map<String, String> issueLinkTemplatePerSystem )
483 {
484 if ( getLog().isDebugEnabled() )
485 {
486 if ( issueLinkTemplatePerSystem == null )
487 {
488 getLog().debug( "No issueLinkTemplatePerSystem configuration was found" );
489 }
490 else
491 {
492 for ( Entry<String, String> entry : issueLinkTemplatePerSystem.entrySet() )
493 {
494 getLog().debug( "issueLinkTemplatePerSystem[" + entry.getKey() + "] = " + entry.getValue() );
495 }
496 }
497 }
498 }
499
500 private boolean generateFeed( final ChangesXML changesXml, final Locale locale )
501 {
502 getLog().debug( "Generating " + feedType + " feed." );
503
504 boolean success = true;
505
506 final FeedGenerator feed = new FeedGenerator( locale );
507 feed.setLink( project.getUrl() + "/changes-report.html" );
508 feed.setTitle( project.getName() + ": " + changesXml.getTitle() );
509 feed.setAuthor( changesXml.getAuthor() );
510 feed.setDateFormat( new SimpleDateFormat( publishDateFormat, new Locale( publishDateLocale ) ) );
511
512 Writer writer = null;
513
514 try
515 {
516 writer = new FileWriter( new File( getReportOutputDirectory(), "changes.rss" ) );
517 feed.export( changesXml.getReleaseList(), feedType, writer );
518 }
519 catch ( IOException ex )
520 {
521 success = false;
522 getLog().warn( "Failed to create rss feed: " + ex.getMessage() );
523 getLog().debug( ex );
524 }
525 finally
526 {
527 try
528 {
529 if ( writer != null )
530 {
531 writer.close();
532 }
533 }
534 catch ( IOException ex )
535 {
536 getLog().warn( "Failed to close writer: " + ex.getMessage() );
537 getLog().debug( ex );
538 }
539 }
540
541 return success;
542 }
543 }