[ https://issues.apache.org/jira/browse/MPLUGIN-442?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=17772825#comment-17772825 ]
ASF GitHub Bot commented on MPLUGIN-442: ---------------------------------------- kwin commented on code in PR #225: URL: https://github.com/apache/maven-plugin-tools/pull/225#discussion_r1349527396 ########## maven-plugin-report-plugin/src/main/java/org/apache/maven/plugin/plugin/report/GoalRenderer.java: ########## @@ -0,0 +1,533 @@ +/* + * 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()), + 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(); + if (StringUtils.isNotEmpty(parameter.getDeprecated())) { + String deprecated = getXhtmlWithValidatedLinks(parameter.getDescription(), context); + description = format("parameter.deprecated", deprecated); + } else 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: remove once migrated to Doxia 2.x + sink.anchor(parameter.getName()); + + startSection(format("parameter.name", parameter.getName())); + sink.anchor_(); + String context = "Parameter " + parameter.getName() + " in goal " + descriptor.getGoal(); + if (StringUtils.isNotEmpty(parameter.getDeprecated())) { + sink.division(); + String deprecated = getXhtmlWithValidatedLinks(parameter.getDeprecated(), context); + sink.rawText(format("parameter.deprecated", deprecated)); + sink.division_(); + } + + 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 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 decoded link (URL encoding is done by the Sink implementation if necessary) + Optional<String> link = Optional.ofNullable(uri).map(u -> { + StringBuilder decodedLink = new StringBuilder(); + if (u.getScheme() != null) { + decodedLink.append(u.getScheme()).append(":"); + } + decodedLink.append(u.getSchemeSpecificPart()); + return decodedLink.toString(); Review Comment: `URI.toString()` returns the URL-encoded String which is a) not necessary as the URL is encoded by https://github.com/apache/maven-doxia/blob/a1e97f9d47b20470c4a5b536406d8c5cb08277bc/doxia-core/src/main/java/org/apache/maven/doxia/sink/impl/Xhtml5BaseSink.java#L1595 b) double encoding potentially leads to invalid links Therefore it is crucial that the decoded URI (i.e. unencoded link) is passed to the Sink API! > Get rid of deprecated XDoc format > --------------------------------- > > Key: MPLUGIN-442 > URL: https://issues.apache.org/jira/browse/MPLUGIN-442 > Project: Maven Plugin Tools > Issue Type: Bug > Components: Plugin Plugin > Reporter: Konrad Windszus > Assignee: Konrad Windszus > Priority: Major > > At some time in the future m-site-p/doxia will no longer support XDoc > (compare with > https://issues.apache.org/jira/browse/DOXIA-569?focusedCommentId=17634481&page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel#comment-17634481). > Therefore the "report" goal should be converted to create "markdown" for the > plugin goal documentation pages instead of "XDoc" in > https://github.com/apache/maven-plugin-tools/blob/master/maven-plugin-tools-generators/src/main/java/org/apache/maven/tools/plugin/generator/PluginXdocGenerator.java. -- This message was sent by Atlassian Jira (v8.20.10#820010)