1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugin.plugin.report;
20
21 import java.io.File;
22 import java.net.URI;
23 import java.net.URISyntaxException;
24 import java.text.MessageFormat;
25 import java.util.AbstractMap.SimpleEntry;
26 import java.util.Collection;
27 import java.util.Collections;
28 import java.util.Iterator;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.Optional;
33 import java.util.regex.Matcher;
34 import java.util.regex.Pattern;
35 import java.util.stream.Collectors;
36
37 import org.apache.commons.lang3.StringUtils;
38 import org.apache.maven.doxia.sink.Sink;
39 import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet.Semantics;
40 import org.apache.maven.doxia.util.DoxiaUtils;
41 import org.apache.maven.plugin.descriptor.MojoDescriptor;
42 import org.apache.maven.plugin.descriptor.Parameter;
43 import org.apache.maven.plugin.logging.Log;
44 import org.apache.maven.project.MavenProject;
45 import org.apache.maven.tools.plugin.EnhancedParameterWrapper;
46 import org.apache.maven.tools.plugin.ExtendedMojoDescriptor;
47 import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
48 import org.apache.maven.tools.plugin.util.PluginUtils;
49 import org.codehaus.plexus.i18n.I18N;
50
51 public class GoalRenderer extends AbstractPluginReportRenderer {
52
53
54 private static final Pattern HTML_LINK_PATTERN = Pattern.compile("<a href=\\\"([^\\\"]*)\\\">(.*?)</a>");
55
56
57 private final File reportOutputDirectory;
58
59 private final MojoDescriptor descriptor;
60 private final boolean disableInternalJavadocLinkValidation;
61
62 private final Log log;
63
64 public GoalRenderer(
65 Sink sink,
66 I18N i18n,
67 Locale locale,
68 MavenProject project,
69 MojoDescriptor descriptor,
70 File reportOutputDirectory,
71 boolean disableInternalJavadocLinkValidation,
72 Log log) {
73 super(sink, locale, i18n, project);
74 this.reportOutputDirectory = reportOutputDirectory;
75 this.descriptor = descriptor;
76 this.disableInternalJavadocLinkValidation = disableInternalJavadocLinkValidation;
77 this.log = log;
78 }
79
80 @Override
81 public String getTitle() {
82 return descriptor.getFullGoalName();
83 }
84
85 @Override
86 protected void renderBody() {
87 startSection(descriptor.getFullGoalName());
88 renderReportNotice();
89 renderDescription("fullname", descriptor.getPluginDescriptor().getId() + ":" + descriptor.getGoal(), false);
90
91 String context = "goal " + descriptor.getGoal();
92 if (StringUtils.isNotEmpty(descriptor.getDeprecated())) {
93 renderDescription("deprecated", getXhtmlWithValidatedLinks(descriptor.getDeprecated(), context), true);
94 }
95 if (StringUtils.isNotEmpty(descriptor.getDescription())) {
96 renderDescription("description", getXhtmlWithValidatedLinks(descriptor.getDescription(), context), true);
97 } else {
98 renderDescription("description", getI18nString("nodescription"), false);
99 }
100 renderAttributes();
101
102 List<Parameter> parameterList = filterParameters(
103 descriptor.getParameters() != null ? descriptor.getParameters() : Collections.emptyList());
104 if (parameterList.isEmpty()) {
105 startSection(getI18nString("parameters"));
106 sink.paragraph();
107 sink.text(getI18nString("noParameter"));
108 sink.paragraph_();
109 endSection();
110 } else {
111 renderParameterOverviewTable(
112 getI18nString("requiredParameters"),
113 parameterList.stream().filter(Parameter::isRequired).iterator());
114 renderParameterOverviewTable(
115 getI18nString("optionalParameters"),
116 parameterList.stream().filter(p -> !p.isRequired()).iterator());
117 renderParameterDetails(parameterList.iterator());
118 }
119 endSection();
120 }
121
122
123
124
125
126 private static List<Parameter> filterParameters(Collection<Parameter> parameterList) {
127 return parameterList.stream()
128 .filter(p -> p.isEditable()
129 && (p.getExpression() == null || !p.getExpression().startsWith("${component.")))
130 .collect(Collectors.toList());
131 }
132
133 private void renderReportNotice() {
134 if (PluginUtils.isMavenReport(descriptor.getImplementation(), project)) {
135 renderDescription("notice.prefix", getI18nString("notice.isMavenReport"), false);
136 }
137 }
138
139
140
141
142 private void renderDescription(String prefixKey, String description, boolean isHtmlMarkup) {
143
144 renderDescriptionPrefix(prefixKey);
145 sink.paragraph();
146 if (isHtmlMarkup) {
147 sink.rawText(description);
148 } else {
149 sink.text(description);
150 }
151 sink.paragraph_();
152 }
153
154 private void renderDescriptionPrefix(String prefixKey) {
155 sink.paragraph();
156 sink.inline(Semantics.STRONG);
157 sink.text(getI18nString(prefixKey));
158 sink.inline_();
159 sink.text(":");
160 sink.paragraph_();
161 }
162
163 @SuppressWarnings("deprecation")
164 private void renderAttributes() {
165 renderDescriptionPrefix("attributes");
166 sink.list();
167
168 renderAttribute(descriptor.isProjectRequired(), "projectRequired");
169 renderAttribute(descriptor.isRequiresReports(), "reportingMojo");
170 renderAttribute(descriptor.isAggregator(), "aggregator");
171 renderAttribute(descriptor.isDirectInvocationOnly(), "directInvocationOnly");
172 renderAttribute(descriptor.isDependencyResolutionRequired(), "dependencyResolutionRequired");
173
174 if (descriptor instanceof ExtendedMojoDescriptor) {
175 ExtendedMojoDescriptor extendedDescriptor = (ExtendedMojoDescriptor) descriptor;
176 renderAttribute(extendedDescriptor.getDependencyCollectionRequired(), "dependencyCollectionRequired");
177 }
178
179 renderAttribute(descriptor.isThreadSafe(), "threadSafe");
180 renderAttribute(!descriptor.isThreadSafe(), "notThreadSafe");
181 renderAttribute(descriptor.getSince(), "since");
182 renderAttribute(descriptor.getPhase(), "phase");
183 renderAttribute(descriptor.getExecutePhase(), "executePhase");
184 renderAttribute(descriptor.getExecuteGoal(), "executeGoal");
185 renderAttribute(descriptor.getExecuteLifecycle(), "executeLifecycle");
186 renderAttribute(descriptor.isOnlineRequired(), "onlineRequired");
187 renderAttribute(!descriptor.isInheritedByDefault(), "notInheritedByDefault");
188
189 sink.list_();
190 }
191
192 private void renderAttribute(boolean condition, String attributeKey) {
193 renderAttribute(condition, attributeKey, Optional.empty());
194 }
195
196 private void renderAttribute(String conditionAndCodeArgument, String attributeKey) {
197 renderAttribute(
198 StringUtils.isNotEmpty(conditionAndCodeArgument),
199 attributeKey,
200 Optional.ofNullable(conditionAndCodeArgument));
201 }
202
203 private void renderAttribute(boolean condition, String attributeKey, Optional<String> codeArgument) {
204 if (condition) {
205 sink.listItem();
206 linkPatternedText(getI18nString(attributeKey));
207 if (codeArgument.isPresent()) {
208 text(": ");
209 sink.inline(Semantics.CODE);
210 sink.text(codeArgument.get());
211 sink.inline_();
212 }
213 text(".");
214 sink.listItem_();
215 }
216 }
217
218 private void renderParameterOverviewTable(String title, Iterator<Parameter> parameters) {
219
220 if (!parameters.hasNext()) {
221 return;
222 }
223 startSection(title);
224 startTable();
225 tableHeader(new String[] {
226 getI18nString("parameter.name.header"),
227 getI18nString("parameter.type.header"),
228 getI18nString("parameter.since.header"),
229 getI18nString("parameter.description.header")
230 });
231 while (parameters.hasNext()) {
232 renderParameterOverviewTableRow(parameters.next());
233 }
234 endTable();
235 endSection();
236 }
237
238 private void renderTableCellWithCode(String text) {
239 renderTableCellWithCode(text, Optional.empty());
240 }
241
242 private void renderTableCellWithCode(String text, Optional<String> link) {
243 sink.tableCell();
244 if (link.isPresent()) {
245 sink.link(link.get(), null);
246 }
247 sink.inline(Semantics.CODE);
248 sink.text(text);
249 sink.inline_();
250 if (link.isPresent()) {
251 sink.link_();
252 }
253 sink.tableCell_();
254 }
255
256 private void renderParameterOverviewTableRow(Parameter parameter) {
257 sink.tableRow();
258
259
260 renderTableCellWithCode(
261 format("parameter.name", parameter.getName()),
262
263 Optional.of("#" + DoxiaUtils.encodeId(parameter.getName())));
264
265
266 Map.Entry<String, Optional<String>> type = getLinkedType(parameter, true);
267 renderTableCellWithCode(type.getKey(), type.getValue());
268
269
270 String since = StringUtils.defaultIfEmpty(parameter.getSince(), "-");
271 renderTableCellWithCode(since);
272
273
274 sink.tableCell();
275 String description;
276 String context = "Parameter " + parameter.getName() + " in goal " + descriptor.getGoal();
277 renderDeprecatedParameterDescription(parameter.getDeprecated(), context);
278 if (StringUtils.isNotEmpty(parameter.getDescription())) {
279 description = getXhtmlWithValidatedLinks(parameter.getDescription(), context);
280 } else {
281 description = getI18nString("nodescription");
282 }
283 sink.rawText(description);
284 renderTableCellDetail("parameter.defaultValue", parameter.getDefaultValue());
285 renderTableCellDetail("parameter.property", getPropertyFromExpression(parameter.getExpression()));
286 renderTableCellDetail("parameter.alias", parameter.getAlias());
287 sink.tableCell_();
288
289 sink.tableRow_();
290 }
291
292 private void renderParameterDetails(Iterator<Parameter> parameters) {
293
294 startSection(getI18nString("parameter.details"));
295
296 while (parameters.hasNext()) {
297 Parameter parameter = parameters.next();
298
299
300 sink.anchor(parameter.getName());
301 sink.anchor_();
302
303 startSection(format("parameter.name", parameter.getName()));
304 String context = "Parameter " + parameter.getName() + " in goal " + descriptor.getGoal();
305 renderDeprecatedParameterDescription(parameter.getDeprecated(), context);
306 sink.division();
307 if (StringUtils.isNotEmpty(parameter.getDescription())) {
308 sink.rawText(getXhtmlWithValidatedLinks(parameter.getDescription(), context));
309 } else {
310 sink.text(getI18nString("nodescription"));
311 }
312 sink.division_();
313
314 sink.list();
315 Map.Entry<String, Optional<String>> typeAndLink = getLinkedType(parameter, false);
316 renderDetail(getI18nString("parameter.type"), typeAndLink.getKey(), typeAndLink.getValue());
317
318 if (StringUtils.isNotEmpty(parameter.getSince())) {
319 renderDetail(getI18nString("parameter.since"), parameter.getSince());
320 }
321
322 if (parameter.isRequired()) {
323 renderDetail(getI18nString("parameter.required"), getI18nString("yes"));
324 } else {
325 renderDetail(getI18nString("parameter.required"), getI18nString("no"));
326 }
327
328 String expression = parameter.getExpression();
329 String property = getPropertyFromExpression(expression);
330 if (property == null) {
331 renderDetail(getI18nString("parameter.expression"), expression);
332 } else {
333 renderDetail(getI18nString("parameter.property"), property);
334 }
335
336 renderDetail(getI18nString("parameter.defaultValue"), parameter.getDefaultValue());
337
338 renderDetail(getI18nString("parameter.alias"), parameter.getAlias());
339
340 sink.list_();
341
342 if (parameters.hasNext()) {
343 sink.horizontalRule();
344 }
345 endSection();
346 }
347 endSection();
348 }
349
350 private void renderDeprecatedParameterDescription(String deprecated, String context) {
351 if (StringUtils.isNotEmpty(deprecated)) {
352 String deprecatedXhtml = getXhtmlWithValidatedLinks(deprecated, context);
353 sink.division();
354 sink.inline(Semantics.STRONG);
355 sink.text(getI18nString("parameter.deprecated"));
356 sink.inline_();
357 sink.lineBreak();
358 sink.rawText(deprecatedXhtml);
359 sink.division_();
360 sink.lineBreak();
361 }
362 }
363
364 private void renderTableCellDetail(String nameKey, String value) {
365 if (StringUtils.isNotEmpty(value)) {
366 sink.lineBreak();
367 sink.inline(Semantics.STRONG);
368 sink.text(getI18nString(nameKey));
369 sink.inline_();
370 sink.text(": ");
371 sink.inline(Semantics.CODE);
372 sink.text(value);
373 sink.inline_();
374 }
375 }
376
377 private void renderDetail(String param, String value) {
378 renderDetail(param, value, Optional.empty());
379 }
380
381 private void renderDetail(String param, String value, Optional<String> valueLink) {
382 if (value != null && !value.isEmpty()) {
383 sink.listItem();
384 sink.inline(Semantics.STRONG);
385 sink.text(param);
386 sink.inline_();
387 sink.text(": ");
388 if (valueLink.isPresent()) {
389 sink.link(valueLink.get());
390 }
391 sink.inline(Semantics.CODE);
392 sink.text(value);
393 sink.inline_();
394 if (valueLink.isPresent()) {
395 sink.link_();
396 }
397 sink.listItem_();
398 }
399 }
400
401 private static String getPropertyFromExpression(String expression) {
402 if ((expression != null && !expression.isEmpty())
403 && expression.startsWith("${")
404 && expression.endsWith("}")
405 && !expression.substring(2).contains("${")) {
406
407 return expression.substring(2, expression.length() - 1);
408 }
409
410 return null;
411 }
412
413 static String getShortType(String type) {
414
415 int startTypeArguments = type.indexOf('<');
416 if (startTypeArguments == -1) {
417 return getShortTypeOfSimpleType(type);
418 } else {
419 StringBuilder shortType = new StringBuilder();
420 shortType.append(getShortTypeOfSimpleType(type.substring(0, startTypeArguments)));
421 shortType
422 .append("<")
423 .append(getShortTypeOfTypeArgument(type.substring(startTypeArguments + 1, type.lastIndexOf(">"))))
424 .append(">");
425 return shortType.toString();
426 }
427 }
428
429 private static String getShortTypeOfTypeArgument(String type) {
430 String[] typeArguments = type.split(",\\s*");
431 StringBuilder shortType = new StringBuilder();
432 for (int i = 0; i < typeArguments.length; i++) {
433 String typeArgument = typeArguments[i];
434 if (typeArgument.contains("<")) {
435
436 return "...";
437 } else {
438 shortType.append(getShortTypeOfSimpleType(typeArgument));
439 if (i < typeArguments.length - 1) {
440 shortType.append(",");
441 }
442 }
443 }
444 return shortType.toString();
445 }
446
447 private static String getShortTypeOfSimpleType(String type) {
448 int index = type.lastIndexOf('.');
449 return type.substring(index + 1);
450 }
451
452 private Map.Entry<String, Optional<String>> getLinkedType(Parameter parameter, boolean isShortType) {
453 final String typeValue;
454 if (isShortType) {
455 typeValue = getShortType(parameter.getType());
456 } else {
457 typeValue = parameter.getType();
458 }
459 URI uri = null;
460 if (parameter instanceof EnhancedParameterWrapper) {
461 EnhancedParameterWrapper enhancedParameter = (EnhancedParameterWrapper) parameter;
462 if (enhancedParameter.getTypeJavadocUrl() != null) {
463 URI javadocUrl = enhancedParameter.getTypeJavadocUrl();
464
465 if (javadocUrl.isAbsolute()
466 || disableInternalJavadocLinkValidation
467 || JavadocLinkGenerator.isLinkValid(javadocUrl, reportOutputDirectory.toPath())) {
468 uri = enhancedParameter.getTypeJavadocUrl();
469 }
470 }
471 }
472
473 return new SimpleEntry<>(typeValue, Optional.ofNullable(uri).map(URI::toASCIIString));
474 }
475
476 String getXhtmlWithValidatedLinks(String xhtmlText, String context) {
477 if (disableInternalJavadocLinkValidation) {
478 return xhtmlText;
479 }
480 StringBuffer sanitizedXhtmlText = new StringBuffer();
481
482 Matcher matcher = HTML_LINK_PATTERN.matcher(xhtmlText);
483 while (matcher.find()) {
484 URI link;
485 try {
486 link = new URI(matcher.group(1));
487 if (!link.isAbsolute() && !JavadocLinkGenerator.isLinkValid(link, reportOutputDirectory.toPath())) {
488 matcher.appendReplacement(sanitizedXhtmlText, matcher.group(2));
489 log.debug(String.format("Removed invalid link %s in %s", link, context));
490 } else {
491 matcher.appendReplacement(sanitizedXhtmlText, matcher.group(0));
492 }
493 } catch (URISyntaxException e) {
494 log.warn(String.format(
495 "Invalid URI %s found in %s. Cannot validate, leave untouched", matcher.group(1), context));
496 matcher.appendReplacement(sanitizedXhtmlText, matcher.group(0));
497 }
498 }
499 matcher.appendTail(sanitizedXhtmlText);
500 return sanitizedXhtmlText.toString();
501 }
502
503
504
505
506
507
508
509 private String format(String key, Object arg1) {
510 return format(key, new Object[] {arg1});
511 }
512
513
514
515
516
517
518
519 private String format(String key, Object[] args) {
520 String pattern = getI18nString(key);
521
522 pattern = StringUtils.replace(pattern, "'", "''");
523
524 MessageFormat messageFormat = new MessageFormat(pattern, locale);
525 return messageFormat.format(args);
526 }
527
528 @Override
529 protected String getI18nSection() {
530 return "plugin.goal";
531 }
532 }