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}