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 }