1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugins.changes;
20
21 import javax.inject.Inject;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.Writer;
26 import java.net.URL;
27 import java.nio.charset.StandardCharsets;
28 import java.nio.file.Files;
29 import java.nio.file.Path;
30 import java.text.SimpleDateFormat;
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.Map.Entry;
37 import java.util.Properties;
38 import java.util.ResourceBundle;
39
40 import org.apache.commons.collections4.map.CaseInsensitiveMap;
41 import org.apache.commons.io.input.XmlStreamReader;
42 import org.apache.maven.plugins.annotations.Mojo;
43 import org.apache.maven.plugins.annotations.Parameter;
44 import org.apache.maven.plugins.changes.model.Release;
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 import org.codehaus.plexus.util.FileUtils;
51
52
53
54
55
56
57
58 @Mojo(name = "changes", threadSafe = true)
59 public class ChangesReport extends AbstractChangesReport {
60
61
62
63
64
65
66 @Parameter(defaultValue = "false")
67 private boolean aggregated;
68
69
70
71
72
73
74
75 @Parameter(property = "changes.addActionDate", defaultValue = "false")
76 private boolean addActionDate;
77
78
79
80
81
82
83
84
85
86
87
88
89
90 @Parameter(defaultValue = "true")
91 private boolean escapeText;
92
93
94
95
96
97
98 @Parameter(defaultValue = "${project.build.directory}/changes", required = true, readonly = true)
99 private File filteredOutputDirectory;
100
101
102
103
104
105
106 @Parameter(defaultValue = "false")
107 private boolean filteringChanges;
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128 @Parameter
129 private Map<String, String> issueLinkTemplatePerSystem;
130
131
132
133
134
135
136
137 @Parameter(defaultValue = "yyyy-MM-dd")
138 private String publishDateFormat;
139
140
141
142
143
144
145
146 @Parameter(defaultValue = "en")
147 private String publishDateLocale;
148
149
150
151
152 @Parameter(defaultValue = "${project.issueManagement.system}", readonly = true)
153 private String system;
154
155
156
157
158
159
160
161 @Parameter(defaultValue = "team.html")
162 private String team;
163
164 @Parameter(defaultValue = "${project.issueManagement.url}", readonly = true)
165 private String url;
166
167
168
169
170
171
172
173
174
175
176
177
178
179 @Parameter
180 private String feedType;
181
182
183
184
185 @Parameter(property = "changes.xmlPath", defaultValue = "src/changes/changes.xml")
186 private File xmlPath;
187
188 private final MavenFileFilter mavenFileFilter;
189
190 @Inject
191 public ChangesReport(MavenFileFilter mavenFileFilter) {
192 this.mavenFileFilter = mavenFileFilter;
193 }
194
195
196
197
198
199 @Override
200 public boolean canGenerateReport() {
201
202 if (runOnlyAtExecutionRoot && !isThisTheExecutionRoot()) {
203 getLog().info("Skipping the Changes Report in this project because it's not the Execution Root");
204 return false;
205 }
206 return xmlPath.isFile();
207 }
208
209 @Override
210 public void executeReport(Locale locale) throws MavenReportException {
211 Date now = new Date();
212 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(publishDateFormat, new Locale(publishDateLocale));
213 Properties additionalProperties = new Properties();
214 additionalProperties.put("publishDate", simpleDateFormat.format(now));
215
216 ChangesXML changesXml = getChangesFromFile(xmlPath, project, additionalProperties);
217 if (changesXml == null) {
218 return;
219 }
220
221 if (aggregated) {
222 final String basePath = project.getBasedir().getAbsolutePath();
223 final String absolutePath = xmlPath.getAbsolutePath();
224 if (!absolutePath.startsWith(basePath)) {
225 getLog().warn("xmlPath should be within the project dir for aggregated changes report.");
226 return;
227 }
228 final String relativePath = absolutePath.substring(basePath.length());
229
230 List<Release> releaseList = changesXml.getReleaseList();
231 for (MavenProject childProject : project.getCollectedProjects()) {
232 final File changesFile = new File(childProject.getBasedir(), relativePath);
233 final ChangesXML childXml = getChangesFromFile(changesFile, childProject, additionalProperties);
234 if (childXml != null) {
235 releaseList =
236 ReleaseUtils.mergeReleases(releaseList, childProject.getName(), childXml.getReleaseList());
237 }
238 }
239 changesXml.setReleaseList(releaseList);
240 }
241
242 ChangesReportRenderer report = new ChangesReportRenderer(getSink(), getBundle(locale), changesXml);
243
244 report.setIssueLinksPerSystem(prepareIssueLinksPerSystem());
245 report.setSystem(system);
246 report.setTeam(team);
247 report.setUrl(url);
248 report.setAddActionDate(addActionDate);
249
250 if (url == null || url.isEmpty()) {
251 getLog().warn("No issue management URL defined in POM. Links to your issues will not work correctly.");
252 }
253
254 boolean feedGenerated = false;
255
256 if (feedType != null && !feedType.isEmpty()) {
257 feedGenerated = generateFeed(changesXml, locale);
258 }
259
260 report.setLinkToFeed(feedGenerated);
261 report.setEscapeText(escapeText);
262
263 report.render();
264
265
266 copyStaticResources();
267 }
268
269 private Map<String, String> prepareIssueLinksPerSystem() {
270 Map<String, String> issueLinkTemplate;
271
272
273 if (this.issueLinkTemplatePerSystem == null) {
274 issueLinkTemplate = new CaseInsensitiveMap<>();
275 } else {
276 issueLinkTemplate = new CaseInsensitiveMap<>(this.issueLinkTemplatePerSystem);
277 }
278
279
280 issueLinkTemplate.computeIfAbsent(
281 ChangesReportRenderer.DEFAULT_ISSUE_SYSTEM_KEY, k -> "%URL%/ViewIssue.jspa?key=%ISSUE%");
282 issueLinkTemplate.computeIfAbsent("Bitbucket", k -> "%URL%/issue/%ISSUE%");
283 issueLinkTemplate.computeIfAbsent("Bugzilla", k -> "%URL%/show_bug.cgi?id=%ISSUE%");
284 issueLinkTemplate.computeIfAbsent("GitHub", k -> "%URL%/%ISSUE%");
285 issueLinkTemplate.computeIfAbsent("GoogleCode", k -> "%URL%/detail?id=%ISSUE%");
286 issueLinkTemplate.computeIfAbsent("JIRA", k -> "%URL%/%ISSUE%");
287 issueLinkTemplate.computeIfAbsent("Mantis", k -> "%URL%/view.php?id=%ISSUE%");
288 issueLinkTemplate.computeIfAbsent("MKS", k -> "%URL%/viewissue?selection=%ISSUE%");
289 issueLinkTemplate.computeIfAbsent("Redmine", k -> "%URL%/issues/show/%ISSUE%");
290 issueLinkTemplate.computeIfAbsent("Scarab", k -> "%URL%/issues/id/%ISSUE%");
291 issueLinkTemplate.computeIfAbsent("SourceForge", k -> "http://sourceforge.net/support/tracker.php?aid=%ISSUE%");
292 issueLinkTemplate.computeIfAbsent("SourceForge2", k -> "%URL%/%ISSUE%");
293 issueLinkTemplate.computeIfAbsent("Trac", k -> "%URL%/ticket/%ISSUE%");
294 issueLinkTemplate.computeIfAbsent("Trackplus", k -> "%URL%/printItem.action?key=%ISSUE%");
295 issueLinkTemplate.computeIfAbsent("Tuleap", k -> "%URL%/?aid=%ISSUE%");
296 issueLinkTemplate.computeIfAbsent("YouTrack", k -> "%URL%/issue/%ISSUE%");
297
298
299
300
301 logIssueLinkTemplatePerSystem(issueLinkTemplate);
302 return issueLinkTemplate;
303 }
304
305 @Override
306 public String getDescription(Locale locale) {
307 return getBundle(locale).getString("report.issues.description");
308 }
309
310 @Override
311 public String getName(Locale locale) {
312 return getBundle(locale).getString("report.issues.name");
313 }
314
315 @Override
316 @Deprecated
317 public String getOutputName() {
318 return "changes";
319 }
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335 private ChangesXML getChangesFromFile(File changesXml, MavenProject project, Properties additionalProperties)
336 throws MavenReportException {
337 if (!changesXml.exists()) {
338 getLog().warn("changes.xml file " + changesXml.getAbsolutePath() + " does not exist.");
339 return null;
340 }
341
342 if (filteringChanges) {
343 if (!filteredOutputDirectory.exists()) {
344 filteredOutputDirectory.mkdirs();
345 }
346 try {
347
348 try (XmlStreamReader xmlStreamReader =
349 XmlStreamReader.builder().setFile(changesXml).get()) {
350 String encoding = xmlStreamReader.getEncoding();
351 File resultFile = new File(
352 filteredOutputDirectory,
353 project.getGroupId() + "." + project.getArtifactId() + "-changes.xml");
354
355 final MavenFileFilterRequest mavenFileFilterRequest = new MavenFileFilterRequest(
356 changesXml,
357 resultFile,
358 true,
359 project,
360 Collections.emptyList(),
361 false,
362 encoding,
363 mavenSession,
364 additionalProperties);
365 mavenFileFilter.copyFile(mavenFileFilterRequest);
366 changesXml = resultFile;
367 }
368 } catch (IOException | MavenFilteringException e) {
369 throw new MavenReportException("Exception during filtering changes file : " + e.getMessage(), e);
370 }
371 }
372 return new ChangesXML(changesXml, getLog());
373 }
374
375 private void copyStaticResources() throws MavenReportException {
376 final String pluginResourcesBase = "org/apache/maven/plugins/changes";
377 String[] resourceNames = {
378 "images/add.gif",
379 "images/fix.gif",
380 "images/icon_help_sml.gif",
381 "images/remove.gif",
382 "images/rss.png",
383 "images/update.gif"
384 };
385 try {
386 getLog().debug("Copying static resources.");
387 for (String resourceName : resourceNames) {
388 URL url = this.getClass().getClassLoader().getResource(pluginResourcesBase + "/" + resourceName);
389 FileUtils.copyURLToFile(url, new File(getReportOutputDirectory(), resourceName));
390 }
391 } catch (IOException e) {
392 throw new MavenReportException("Unable to copy static resources.");
393 }
394 }
395
396 private ResourceBundle getBundle(Locale locale) {
397 return ResourceBundle.getBundle(
398 "changes-report", locale, this.getClass().getClassLoader());
399 }
400
401 private void logIssueLinkTemplatePerSystem(Map<String, String> issueLinkTemplatePerSystem) {
402 if (getLog().isDebugEnabled()) {
403 for (Entry<String, String> entry : issueLinkTemplatePerSystem.entrySet()) {
404 getLog().debug("issueLinkTemplatePerSystem[" + entry.getKey() + "] = " + entry.getValue());
405 }
406 }
407 }
408
409 private boolean generateFeed(final ChangesXML changesXml, final Locale locale) {
410 getLog().debug("Generating " + feedType + " feed.");
411
412 boolean success = true;
413
414 final FeedGenerator feed = new FeedGenerator(locale);
415 feed.setLink(project.getUrl() + "/changes-report.html");
416 feed.setTitle(project.getName() + ": " + changesXml.getTitle());
417 feed.setAuthor(changesXml.getAuthor());
418 feed.setDateFormat(new SimpleDateFormat(publishDateFormat, new Locale(publishDateLocale)));
419
420 Path changes = getReportOutputDirectory().toPath().resolve("changes.rss");
421 try (Writer writer = Files.newBufferedWriter(changes, StandardCharsets.UTF_8)) {
422 feed.export(changesXml.getReleaseList(), feedType, writer);
423 } catch (IOException ex) {
424 success = false;
425 getLog().warn("Failed to create RSS feed: " + ex.getMessage());
426 getLog().debug(ex);
427 }
428
429 return success;
430 }
431 }