View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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.HtmlTools;
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      /** Regular expression matching an XHTML link with group 1 = link target, group 2 = link label. */
54      private static final Pattern HTML_LINK_PATTERN = Pattern.compile("<a href=\\\"([^\\\"]*)\\\">(.*?)</a>");
55  
56      /** The directory where the generated site is written. Used for resolving relative links to javadoc. */
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     /** Filter parameters to only retain those which must be documented, i.e. neither components nor read-only ones.
123      *
124      * @param parameterList not null
125      * @return the parameters list without components. */
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      * A description consists of a term/prefix and the actual description text
141      */
142     private void renderDescription(String prefixKey, String description, boolean isHtmlMarkup) {
143         // TODO: convert to dt and dd elements
144         renderDescriptionPrefix(prefixKey);
145         sink.paragraph();
146         if (isHtmlMarkup) {
147             sink.rawText(description);
148         } else {
149             sink.text(description);
150         }
151         sink.paragraph_(); // p
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         // don't emit empty tables
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         // name
259         // link to appropriate section
260         renderTableCellWithCode(
261                 format("parameter.name", parameter.getName()),
262                 // no need for additional URI encoding as it returns only URI safe characters
263                 Optional.of("#" + HtmlTools.encodeId(parameter.getName())));
264 
265         // type
266         Map.Entry<String, Optional<String>> type = getLinkedType(parameter, true);
267         renderTableCellWithCode(type.getKey(), type.getValue());
268 
269         // since
270         String since = StringUtils.defaultIfEmpty(parameter.getSince(), "-");
271         renderTableCellWithCode(since);
272 
273         // description
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             // deprecated anchor for backwards-compatibility with XDoc (upper and lower case)
299             // TODO: remove once migrated to Doxia 2.x
300             sink.anchor(parameter.getName());
301 
302             startSection(format("parameter.name", parameter.getName()));
303             sink.anchor_();
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_(); // ul
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             // expression="${xxx}" -> property="xxx"
407             return expression.substring(2, expression.length() - 1);
408         }
409         // no property can be extracted
410         return null;
411     }
412 
413     static String getShortType(String type) {
414         // split into type arguments and main type
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                 // nested type arguments lead to ellipsis
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                 // optionally check if link is valid
465                 if (javadocUrl.isAbsolute()
466                         || disableInternalJavadocLinkValidation
467                         || JavadocLinkGenerator.isLinkValid(javadocUrl, reportOutputDirectory.toPath())) {
468                     uri = enhancedParameter.getTypeJavadocUrl();
469                 }
470             }
471         }
472         // rely on the encoded URI
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         // find all links which are not absolute
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     /** Convenience method.
504      *
505      * @param key  not null
506      * @param arg1 not null
507      * @return Localized, formatted text identified by <code>key</code>.
508      * @see #format(String, Object[]) */
509     private String format(String key, Object arg1) {
510         return format(key, new Object[] {arg1});
511     }
512 
513     /** Looks up the value for <code>key</code> in the <code>ResourceBundle</code>, then formats that value for the specified
514      * <code>Locale</code> using <code>args</code>.
515      *
516      * @param key  not null
517      * @param args not null
518      * @return Localized, formatted text identified by <code>key</code>. */
519     private String format(String key, Object[] args) {
520         String pattern = getI18nString(key);
521         // we don't need quoting so spare us the confusion in the resource bundle to double them up in some keys
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 }