1 package com.guinetik.terminaljavadocs.plugin;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.nio.charset.StandardCharsets;
7 import java.nio.file.Files;
8 import java.nio.file.Path;
9 import java.nio.file.Paths;
10 import java.util.ArrayList;
11 import java.util.List;
12 import org.apache.maven.execution.MavenSession;
13 import org.apache.maven.plugin.AbstractMojo;
14 import org.apache.maven.plugin.MojoExecutionException;
15 import org.apache.maven.plugins.annotations.Mojo;
16 import org.apache.maven.plugins.annotations.Parameter;
17 import org.apache.maven.project.MavenProject;
18
19
20
21
22
23 @Mojo(
24 name = "generate-landing-pages",
25 defaultPhase = org.apache.maven.plugins.annotations.LifecyclePhase.POST_SITE,
26 aggregator = true
27 )
28 public class GenerateLandingPagesMojo extends AbstractMojo {
29
30
31
32
33 @Parameter(defaultValue = "${session}", readonly = true, required = true)
34 private MavenSession session;
35
36
37
38
39 @Parameter(defaultValue = "${project}", readonly = true, required = true)
40 private MavenProject project;
41
42
43
44
45
46 @Parameter(
47 property = "terminaljavadocs.project.name",
48 defaultValue = "${project.name}"
49 )
50 private String projectName;
51
52
53
54
55
56 @Parameter(property = "terminaljavadocs.skip", defaultValue = "false")
57 private boolean skip;
58
59
60
61
62 @Parameter(defaultValue = "${project.build.directory}", readonly = true)
63 private File buildDirectory;
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82 @Override
83 public void execute() throws MojoExecutionException {
84 if (skip) {
85 getLog().info("Skipping landing page generation");
86 return;
87 }
88
89
90 if (!"pom".equals(project.getPackaging())) {
91 getLog().debug(
92 "Skipping landing page generation: not a POM project"
93 );
94 return;
95 }
96
97 try {
98 List<ModuleReport> coverageModules = new ArrayList<>();
99 List<ModuleReport> xrefModules = new ArrayList<>();
100
101
102 List<MavenProject> projects = session.getProjects();
103 getLog().info(
104 "Scanning " + projects.size() + " reactor projects for reports"
105 );
106 for (MavenProject reactorProject : projects) {
107 String artifactId = reactorProject.getArtifactId();
108 String description = reactorProject.getDescription() != null
109 ? reactorProject.getDescription()
110 : "";
111 String buildDir = reactorProject.getBuild().getDirectory();
112
113
114 File jacocoIndex = new File(buildDir, "staging/jacoco/index.html");
115 if (!jacocoIndex.exists()) {
116 jacocoIndex = new File(buildDir, "site/jacoco/index.html");
117 }
118 getLog().debug(
119 "Checking for coverage in " + artifactId + " at: " + jacocoIndex.getAbsolutePath()
120 );
121 if (jacocoIndex.exists()) {
122 coverageModules.add(
123 new ModuleReport(artifactId, description, artifactId)
124 );
125 getLog().info("Found coverage report in: " + artifactId);
126 } else {
127 getLog().debug(
128 "No coverage report found for " + artifactId
129 );
130 }
131
132
133
134 File xrefIndex = new File(buildDir, "staging/xref/overview-summary.html");
135 if (!xrefIndex.exists()) {
136 xrefIndex = new File(buildDir, "site/xref/overview-summary.html");
137 }
138 getLog().debug(
139 "Checking for xref in " + artifactId + " at: " + xrefIndex.getAbsolutePath()
140 );
141 if (xrefIndex.exists()) {
142 xrefModules.add(
143 new ModuleReport(artifactId, description, artifactId)
144 );
145 getLog().info("Found xref report in: " + artifactId);
146 } else {
147 getLog().debug(
148 "No xref report found for " + artifactId
149 );
150 }
151 }
152
153
154 File outputDir = new File(buildDirectory, "staging");
155 if (!outputDir.exists()) {
156 outputDir = new File(buildDirectory, "site");
157 }
158 outputDir.mkdirs();
159
160
161 if (!coverageModules.isEmpty()) {
162 String coverageHtml = generateCoveragePage(
163 coverageModules,
164 projectName
165 );
166 Path coveragePath = Paths.get(
167 outputDir.getAbsolutePath(),
168 "coverage.html"
169 );
170 Files.write(
171 coveragePath,
172 coverageHtml.getBytes(StandardCharsets.UTF_8)
173 );
174 getLog().info(
175 "Generated coverage landing page: " + coveragePath
176 );
177 }
178
179
180 if (!xrefModules.isEmpty()) {
181 String xrefHtml = generateXrefPage(xrefModules, projectName);
182 Path xrefPath = Paths.get(
183 outputDir.getAbsolutePath(),
184 "source-xref.html"
185 );
186 Files.write(
187 xrefPath,
188 xrefHtml.getBytes(StandardCharsets.UTF_8)
189 );
190 getLog().info("Generated xref landing page: " + xrefPath);
191 }
192
193 if (coverageModules.isEmpty() && xrefModules.isEmpty()) {
194 getLog().info("No modules with coverage or xref reports found");
195 }
196 } catch (IOException e) {
197 throw new MojoExecutionException(
198 "Failed to generate landing pages",
199 e
200 );
201 }
202 }
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219 private String generateCoveragePage(
220 List<ModuleReport> modules,
221 String projectName
222 ) throws IOException {
223 String template = loadTemplate("templates/coverage-page.html");
224
225
226 StringBuilder rows = new StringBuilder();
227 for (ModuleReport module : modules) {
228 String row =
229 " <tr>\n" +
230 " <td>" +
231 escapeHtml(module.getArtifactId()) +
232 "</td>\n" +
233 " <td>" +
234 escapeHtml(module.getDescription()) +
235 "</td>\n" +
236 " <td><a href=\"./" +
237 escapeHtml(module.getArtifactId()) +
238 "/jacoco/index.html\">View Coverage →</a></td>\n" +
239 " </tr>\n";
240 rows.append(row);
241 }
242
243
244 String result = template
245 .replace("{{project.name}}", escapeHtml(projectName))
246 .replace("{{module-rows}}", rows.toString());
247
248 return result;
249 }
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266 private String generateXrefPage(
267 List<ModuleReport> modules,
268 String projectName
269 ) throws IOException {
270 String template = loadTemplate("templates/xref-page.html");
271
272
273 StringBuilder rows = new StringBuilder();
274 for (ModuleReport module : modules) {
275 String row =
276 " <tr>\n" +
277 " <td>" +
278 escapeHtml(module.getArtifactId()) +
279 "</td>\n" +
280 " <td>" +
281 escapeHtml(module.getDescription()) +
282 "</td>\n" +
283 " <td><a href=\"./" +
284 escapeHtml(module.getArtifactId()) +
285 "/xref/overview-summary.html\">Browse Source →</a></td>\n" +
286 " </tr>\n";
287 rows.append(row);
288 }
289
290
291 String result = template
292 .replace("{{project.name}}", escapeHtml(projectName))
293 .replace("{{module-rows}}", rows.toString());
294
295 return result;
296 }
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319 private String loadTemplate(String resourcePath) throws IOException {
320
321 InputStream is = null;
322
323
324 is = Thread.currentThread()
325 .getContextClassLoader()
326 .getResourceAsStream(resourcePath);
327
328
329 if (is == null) {
330 is =
331 GenerateLandingPagesMojo.class.getClassLoader().getResourceAsStream(
332 resourcePath
333 );
334 }
335
336
337 if (is == null) {
338 is = GenerateLandingPagesMojo.class.getResourceAsStream(
339 "/" + resourcePath
340 );
341 }
342
343 if (is != null) {
344 return new String(is.readAllBytes(), StandardCharsets.UTF_8);
345 }
346
347
348
349 try {
350
351
352 String classPath =
353 GenerateLandingPagesMojo.class.getProtectionDomain()
354 .getCodeSource()
355 .getLocation()
356 .getPath();
357
358
359 if (classPath.contains("target" + File.separator + "classes")) {
360 Path targetClasses = Paths.get(classPath);
361 Path pluginModule = targetClasses.getParent().getParent();
362 Path templatePath = pluginModule.resolve(
363 "src/main/resources/" + resourcePath
364 );
365
366 if (Files.exists(templatePath)) {
367 getLog().debug(
368 "Loading template from filesystem: " + templatePath
369 );
370 return new String(
371 Files.readAllBytes(templatePath),
372 StandardCharsets.UTF_8
373 );
374 }
375 }
376 } catch (Exception e) {
377 getLog().debug(
378 "Could not load template from filesystem: " + e.getMessage()
379 );
380 }
381
382 throw new IOException(
383 "Template not found: " +
384 resourcePath +
385 ". Make sure templates are in src/main/resources/ or packaged in the plugin JAR."
386 );
387 }
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405 private String escapeHtml(String text) {
406 if (text == null) {
407 return "";
408 }
409 return text
410 .replace("&", "&")
411 .replace("<", "<")
412 .replace(">", ">")
413 .replace("\"", """)
414 .replace("'", "'");
415 }
416 }