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