blob: 71a62f10fb533b6f6782284f52524b0e1ca36615 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.plugin.plugin.report;
import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.AbstractMap.SimpleEntry;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.doxia.sink.Sink;
import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet.Semantics;
import org.apache.maven.doxia.util.HtmlTools;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.Parameter;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.tools.plugin.EnhancedParameterWrapper;
import org.apache.maven.tools.plugin.ExtendedMojoDescriptor;
import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
import org.apache.maven.tools.plugin.util.PluginUtils;
import org.codehaus.plexus.i18n.I18N;
public class GoalRenderer extends AbstractPluginReportRenderer {
/** Regular expression matching an XHTML link with group 1 = link target, group 2 = link label. */
private static final Pattern HTML_LINK_PATTERN = Pattern.compile("<a href=\\\"([^\\\"]*)\\\">(.*?)</a>");
/** The directory where the generated site is written. Used for resolving relative links to javadoc. */
private final File reportOutputDirectory;
private final MojoDescriptor descriptor;
private final boolean disableInternalJavadocLinkValidation;
private final Log log;
public GoalRenderer(
Sink sink,
I18N i18n,
Locale locale,
MavenProject project,
MojoDescriptor descriptor,
File reportOutputDirectory,
boolean disableInternalJavadocLinkValidation,
Log log) {
super(sink, locale, i18n, project);
this.reportOutputDirectory = reportOutputDirectory;
this.descriptor = descriptor;
this.disableInternalJavadocLinkValidation = disableInternalJavadocLinkValidation;
this.log = log;
}
@Override
public String getTitle() {
return descriptor.getFullGoalName();
}
@Override
protected void renderBody() {
startSection(descriptor.getFullGoalName());
renderReportNotice();
renderDescription("fullname", descriptor.getPluginDescriptor().getId() + ":" + descriptor.getGoal(), false);
String context = "goal " + descriptor.getGoal();
if (StringUtils.isNotEmpty(descriptor.getDeprecated())) {
renderDescription("deprecated", getXhtmlWithValidatedLinks(descriptor.getDeprecated(), context), true);
}
if (StringUtils.isNotEmpty(descriptor.getDescription())) {
renderDescription("description", getXhtmlWithValidatedLinks(descriptor.getDescription(), context), true);
} else {
renderDescription("description", getI18nString("nodescription"), false);
}
renderAttributes();
List<Parameter> parameterList = filterParameters(
descriptor.getParameters() != null ? descriptor.getParameters() : Collections.emptyList());
if (parameterList.isEmpty()) {
startSection(getI18nString("parameters"));
sink.paragraph();
sink.text(getI18nString("noParameter"));
sink.paragraph_();
endSection();
} else {
renderParameterOverviewTable(
getI18nString("requiredParameters"),
parameterList.stream().filter(Parameter::isRequired).iterator());
renderParameterOverviewTable(
getI18nString("optionalParameters"),
parameterList.stream().filter(p -> !p.isRequired()).iterator());
renderParameterDetails(parameterList.iterator());
}
endSection();
}
/** Filter parameters to only retain those which must be documented, i.e. neither components nor read-only ones.
*
* @param parameterList not null
* @return the parameters list without components. */
private static List<Parameter> filterParameters(Collection<Parameter> parameterList) {
return parameterList.stream()
.filter(p -> p.isEditable()
&& (p.getExpression() == null || !p.getExpression().startsWith("${component.")))
.collect(Collectors.toList());
}
private void renderReportNotice() {
if (PluginUtils.isMavenReport(descriptor.getImplementation(), project)) {
renderDescription("notice.prefix", getI18nString("notice.isMavenReport"), false);
}
}
/**
* A description consists of a term/prefix and the actual description text
*/
private void renderDescription(String prefixKey, String description, boolean isHtmlMarkup) {
// TODO: convert to dt and dd elements
renderDescriptionPrefix(prefixKey);
sink.paragraph();
if (isHtmlMarkup) {
sink.rawText(description);
} else {
sink.text(description);
}
sink.paragraph_(); // p
}
private void renderDescriptionPrefix(String prefixKey) {
sink.paragraph();
sink.inline(Semantics.STRONG);
sink.text(getI18nString(prefixKey));
sink.inline_();
sink.text(":");
sink.paragraph_();
}
@SuppressWarnings("deprecation")
private void renderAttributes() {
renderDescriptionPrefix("attributes");
sink.list();
renderAttribute(descriptor.isProjectRequired(), "projectRequired");
renderAttribute(descriptor.isRequiresReports(), "reportingMojo");
renderAttribute(descriptor.isAggregator(), "aggregator");
renderAttribute(descriptor.isDirectInvocationOnly(), "directInvocationOnly");
renderAttribute(descriptor.isDependencyResolutionRequired(), "dependencyResolutionRequired");
if (descriptor instanceof ExtendedMojoDescriptor) {
ExtendedMojoDescriptor extendedDescriptor = (ExtendedMojoDescriptor) descriptor;
renderAttribute(extendedDescriptor.getDependencyCollectionRequired(), "dependencyCollectionRequired");
}
renderAttribute(descriptor.isThreadSafe(), "threadSafe");
renderAttribute(!descriptor.isThreadSafe(), "notThreadSafe");
renderAttribute(descriptor.getSince(), "since");
renderAttribute(descriptor.getPhase(), "phase");
renderAttribute(descriptor.getExecutePhase(), "executePhase");
renderAttribute(descriptor.getExecuteGoal(), "executeGoal");
renderAttribute(descriptor.getExecuteLifecycle(), "executeLifecycle");
renderAttribute(descriptor.isOnlineRequired(), "onlineRequired");
renderAttribute(!descriptor.isInheritedByDefault(), "notInheritedByDefault");
sink.list_();
}
private void renderAttribute(boolean condition, String attributeKey) {
renderAttribute(condition, attributeKey, Optional.empty());
}
private void renderAttribute(String conditionAndCodeArgument, String attributeKey) {
renderAttribute(
StringUtils.isNotEmpty(conditionAndCodeArgument),
attributeKey,
Optional.ofNullable(conditionAndCodeArgument));
}
private void renderAttribute(boolean condition, String attributeKey, Optional<String> codeArgument) {
if (condition) {
sink.listItem();
linkPatternedText(getI18nString(attributeKey));
if (codeArgument.isPresent()) {
text(": ");
sink.inline(Semantics.CODE);
sink.text(codeArgument.get());
sink.inline_();
}
text(".");
sink.listItem_();
}
}
private void renderParameterOverviewTable(String title, Iterator<Parameter> parameters) {
// don't emit empty tables
if (!parameters.hasNext()) {
return;
}
startSection(title);
startTable();
tableHeader(new String[] {
getI18nString("parameter.name.header"),
getI18nString("parameter.type.header"),
getI18nString("parameter.since.header"),
getI18nString("parameter.description.header")
});
while (parameters.hasNext()) {
renderParameterOverviewTableRow(parameters.next());
}
endTable();
endSection();
}
private void renderTableCellWithCode(String text) {
renderTableCellWithCode(text, Optional.empty());
}
private void renderTableCellWithCode(String text, Optional<String> link) {
sink.tableCell();
if (link.isPresent()) {
sink.link(link.get(), null);
}
sink.inline(Semantics.CODE);
sink.text(text);
sink.inline_();
if (link.isPresent()) {
sink.link_();
}
sink.tableCell_();
}
private void renderParameterOverviewTableRow(Parameter parameter) {
sink.tableRow();
// name
// link to appropriate section
renderTableCellWithCode(
format("parameter.name", parameter.getName()),
// no need for additional URI encoding as it returns only URI safe characters
Optional.of("#" + HtmlTools.encodeId(parameter.getName())));
// type
Map.Entry<String, Optional<String>> type = getLinkedType(parameter, true);
renderTableCellWithCode(type.getKey(), type.getValue());
// since
String since = StringUtils.defaultIfEmpty(parameter.getSince(), "-");
renderTableCellWithCode(since);
// description
sink.tableCell();
String description;
String context = "Parameter " + parameter.getName() + " in goal " + descriptor.getGoal();
renderDeprecatedParameterDescription(parameter.getDeprecated(), context);
if (StringUtils.isNotEmpty(parameter.getDescription())) {
description = getXhtmlWithValidatedLinks(parameter.getDescription(), context);
} else {
description = getI18nString("nodescription");
}
sink.rawText(description);
renderTableCellDetail("parameter.defaultValue", parameter.getDefaultValue());
renderTableCellDetail("parameter.property", getPropertyFromExpression(parameter.getExpression()));
renderTableCellDetail("parameter.alias", parameter.getAlias());
sink.tableCell_();
sink.tableRow_();
}
private void renderParameterDetails(Iterator<Parameter> parameters) {
startSection(getI18nString("parameter.details"));
while (parameters.hasNext()) {
Parameter parameter = parameters.next();
// deprecated anchor for backwards-compatibility with XDoc (upper and lower case)
// TODO: replace once migrated to Doxia 2.x with two-arg startSection(String, String) method
sink.anchor(parameter.getName());
sink.anchor_();
startSection(format("parameter.name", parameter.getName()));
String context = "Parameter " + parameter.getName() + " in goal " + descriptor.getGoal();
renderDeprecatedParameterDescription(parameter.getDeprecated(), context);
sink.division();
if (StringUtils.isNotEmpty(parameter.getDescription())) {
sink.rawText(getXhtmlWithValidatedLinks(parameter.getDescription(), context));
} else {
sink.text(getI18nString("nodescription"));
}
sink.division_();
sink.list();
Map.Entry<String, Optional<String>> typeAndLink = getLinkedType(parameter, false);
renderDetail(getI18nString("parameter.type"), typeAndLink.getKey(), typeAndLink.getValue());
if (StringUtils.isNotEmpty(parameter.getSince())) {
renderDetail(getI18nString("parameter.since"), parameter.getSince());
}
if (parameter.isRequired()) {
renderDetail(getI18nString("parameter.required"), getI18nString("yes"));
} else {
renderDetail(getI18nString("parameter.required"), getI18nString("no"));
}
String expression = parameter.getExpression();
String property = getPropertyFromExpression(expression);
if (property == null) {
renderDetail(getI18nString("parameter.expression"), expression);
} else {
renderDetail(getI18nString("parameter.property"), property);
}
renderDetail(getI18nString("parameter.defaultValue"), parameter.getDefaultValue());
renderDetail(getI18nString("parameter.alias"), parameter.getAlias());
sink.list_(); // ul
if (parameters.hasNext()) {
sink.horizontalRule();
}
endSection();
}
endSection();
}
private void renderDeprecatedParameterDescription(String deprecated, String context) {
if (StringUtils.isNotEmpty(deprecated)) {
String deprecatedXhtml = getXhtmlWithValidatedLinks(deprecated, context);
sink.division();
sink.inline(Semantics.STRONG);
sink.text(getI18nString("parameter.deprecated"));
sink.inline_();
sink.lineBreak();
sink.rawText(deprecatedXhtml);
sink.division_();
sink.lineBreak();
}
}
private void renderTableCellDetail(String nameKey, String value) {
if (StringUtils.isNotEmpty(value)) {
sink.lineBreak();
sink.inline(Semantics.STRONG);
sink.text(getI18nString(nameKey));
sink.inline_();
sink.text(": ");
sink.inline(Semantics.CODE);
sink.text(value);
sink.inline_();
}
}
private void renderDetail(String param, String value) {
renderDetail(param, value, Optional.empty());
}
private void renderDetail(String param, String value, Optional<String> valueLink) {
if (value != null && !value.isEmpty()) {
sink.listItem();
sink.inline(Semantics.STRONG);
sink.text(param);
sink.inline_();
sink.text(": ");
if (valueLink.isPresent()) {
sink.link(valueLink.get());
}
sink.inline(Semantics.CODE);
sink.text(value);
sink.inline_();
if (valueLink.isPresent()) {
sink.link_();
}
sink.listItem_();
}
}
private static String getPropertyFromExpression(String expression) {
if ((expression != null && !expression.isEmpty())
&& expression.startsWith("${")
&& expression.endsWith("}")
&& !expression.substring(2).contains("${")) {
// expression="${xxx}" -> property="xxx"
return expression.substring(2, expression.length() - 1);
}
// no property can be extracted
return null;
}
static String getShortType(String type) {
// split into type arguments and main type
int startTypeArguments = type.indexOf('<');
if (startTypeArguments == -1) {
return getShortTypeOfSimpleType(type);
} else {
StringBuilder shortType = new StringBuilder();
shortType.append(getShortTypeOfSimpleType(type.substring(0, startTypeArguments)));
shortType
.append("<")
.append(getShortTypeOfTypeArgument(type.substring(startTypeArguments + 1, type.lastIndexOf(">"))))
.append(">");
return shortType.toString();
}
}
private static String getShortTypeOfTypeArgument(String type) {
String[] typeArguments = type.split(",\\s*");
StringBuilder shortType = new StringBuilder();
for (int i = 0; i < typeArguments.length; i++) {
String typeArgument = typeArguments[i];
if (typeArgument.contains("<")) {
// nested type arguments lead to ellipsis
return "...";
} else {
shortType.append(getShortTypeOfSimpleType(typeArgument));
if (i < typeArguments.length - 1) {
shortType.append(",");
}
}
}
return shortType.toString();
}
private static String getShortTypeOfSimpleType(String type) {
int index = type.lastIndexOf('.');
return type.substring(index + 1);
}
private Map.Entry<String, Optional<String>> getLinkedType(Parameter parameter, boolean isShortType) {
final String typeValue;
if (isShortType) {
typeValue = getShortType(parameter.getType());
} else {
typeValue = parameter.getType();
}
URI uri = null;
if (parameter instanceof EnhancedParameterWrapper) {
EnhancedParameterWrapper enhancedParameter = (EnhancedParameterWrapper) parameter;
if (enhancedParameter.getTypeJavadocUrl() != null) {
URI javadocUrl = enhancedParameter.getTypeJavadocUrl();
// optionally check if link is valid
if (javadocUrl.isAbsolute()
|| disableInternalJavadocLinkValidation
|| JavadocLinkGenerator.isLinkValid(javadocUrl, reportOutputDirectory.toPath())) {
uri = enhancedParameter.getTypeJavadocUrl();
}
}
}
// rely on the encoded URI
return new SimpleEntry<>(typeValue, Optional.ofNullable(uri).map(URI::toASCIIString));
}
String getXhtmlWithValidatedLinks(String xhtmlText, String context) {
if (disableInternalJavadocLinkValidation) {
return xhtmlText;
}
StringBuffer sanitizedXhtmlText = new StringBuffer();
// find all links which are not absolute
Matcher matcher = HTML_LINK_PATTERN.matcher(xhtmlText);
while (matcher.find()) {
URI link;
try {
link = new URI(matcher.group(1));
if (!link.isAbsolute() && !JavadocLinkGenerator.isLinkValid(link, reportOutputDirectory.toPath())) {
matcher.appendReplacement(sanitizedXhtmlText, matcher.group(2));
log.debug(String.format("Removed invalid link %s in %s", link, context));
} else {
matcher.appendReplacement(sanitizedXhtmlText, matcher.group(0));
}
} catch (URISyntaxException e) {
log.warn(String.format(
"Invalid URI %s found in %s. Cannot validate, leave untouched", matcher.group(1), context));
matcher.appendReplacement(sanitizedXhtmlText, matcher.group(0));
}
}
matcher.appendTail(sanitizedXhtmlText);
return sanitizedXhtmlText.toString();
}
/** Convenience method.
*
* @param key not null
* @param arg1 not null
* @return Localized, formatted text identified by <code>key</code>.
* @see #format(String, Object[]) */
private String format(String key, Object arg1) {
return format(key, new Object[] {arg1});
}
/** Looks up the value for <code>key</code> in the <code>ResourceBundle</code>, then formats that value for the specified
* <code>Locale</code> using <code>args</code>.
*
* @param key not null
* @param args not null
* @return Localized, formatted text identified by <code>key</code>. */
private String format(String key, Object[] args) {
String pattern = getI18nString(key);
// we don't need quoting so spare us the confusion in the resource bundle to double them up in some keys
pattern = StringUtils.replace(pattern, "'", "''");
MessageFormat messageFormat = new MessageFormat(pattern, locale);
return messageFormat.format(args);
}
@Override
protected String getI18nSection() {
return "plugin.goal";
}
}