001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.plugin.plugin.report; 020 021import java.io.File; 022import java.net.URI; 023import java.net.URISyntaxException; 024import java.text.MessageFormat; 025import java.util.AbstractMap.SimpleEntry; 026import java.util.Collection; 027import java.util.Collections; 028import java.util.Iterator; 029import java.util.List; 030import java.util.Locale; 031import java.util.Map; 032import java.util.Optional; 033import java.util.regex.Matcher; 034import java.util.regex.Pattern; 035import java.util.stream.Collectors; 036 037import org.apache.commons.lang3.StringUtils; 038import org.apache.maven.doxia.sink.Sink; 039import org.apache.maven.doxia.sink.impl.SinkEventAttributeSet.Semantics; 040import org.apache.maven.doxia.util.HtmlTools; 041import org.apache.maven.plugin.descriptor.MojoDescriptor; 042import org.apache.maven.plugin.descriptor.Parameter; 043import org.apache.maven.plugin.logging.Log; 044import org.apache.maven.project.MavenProject; 045import org.apache.maven.tools.plugin.EnhancedParameterWrapper; 046import org.apache.maven.tools.plugin.ExtendedMojoDescriptor; 047import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator; 048import org.apache.maven.tools.plugin.util.PluginUtils; 049import org.codehaus.plexus.i18n.I18N; 050 051public class GoalRenderer extends AbstractPluginReportRenderer { 052 053 /** Regular expression matching an XHTML link with group 1 = link target, group 2 = link label. */ 054 private static final Pattern HTML_LINK_PATTERN = Pattern.compile("<a href=\\\"([^\\\"]*)\\\">(.*?)</a>"); 055 056 /** The directory where the generated site is written. Used for resolving relative links to javadoc. */ 057 private final File reportOutputDirectory; 058 059 private final MojoDescriptor descriptor; 060 private final boolean disableInternalJavadocLinkValidation; 061 062 private final Log log; 063 064 public GoalRenderer( 065 Sink sink, 066 I18N i18n, 067 Locale locale, 068 MavenProject project, 069 MojoDescriptor descriptor, 070 File reportOutputDirectory, 071 boolean disableInternalJavadocLinkValidation, 072 Log log) { 073 super(sink, locale, i18n, project); 074 this.reportOutputDirectory = reportOutputDirectory; 075 this.descriptor = descriptor; 076 this.disableInternalJavadocLinkValidation = disableInternalJavadocLinkValidation; 077 this.log = log; 078 } 079 080 @Override 081 public String getTitle() { 082 return descriptor.getFullGoalName(); 083 } 084 085 @Override 086 protected void renderBody() { 087 startSection(descriptor.getFullGoalName()); 088 renderReportNotice(); 089 renderDescription("fullname", descriptor.getPluginDescriptor().getId() + ":" + descriptor.getGoal(), false); 090 091 String context = "goal " + descriptor.getGoal(); 092 if (StringUtils.isNotEmpty(descriptor.getDeprecated())) { 093 renderDescription("deprecated", getXhtmlWithValidatedLinks(descriptor.getDeprecated(), context), true); 094 } 095 if (StringUtils.isNotEmpty(descriptor.getDescription())) { 096 renderDescription("description", getXhtmlWithValidatedLinks(descriptor.getDescription(), context), true); 097 } else { 098 renderDescription("description", getI18nString("nodescription"), false); 099 } 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: replace once migrated to Doxia 2.x with two-arg startSection(String, String) method 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_(); // 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}