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.tools.plugin.generator;
020
021import java.io.File;
022import java.io.IOException;
023import java.io.OutputStreamWriter;
024import java.io.Writer;
025import java.net.URI;
026import java.util.LinkedHashMap;
027import java.util.LinkedHashSet;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031
032import org.apache.maven.plugin.descriptor.MojoDescriptor;
033import org.apache.maven.plugin.descriptor.Parameter;
034import org.apache.maven.plugin.descriptor.PluginDescriptor;
035import org.apache.maven.plugin.descriptor.Requirement;
036import org.apache.maven.project.MavenProject;
037import org.apache.maven.tools.plugin.ExtendedMojoDescriptor;
038import org.apache.maven.tools.plugin.ExtendedPluginDescriptor;
039import org.apache.maven.tools.plugin.PluginToolsRequest;
040import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
041import org.apache.maven.tools.plugin.util.PluginUtils;
042import org.codehaus.plexus.util.StringUtils;
043import org.codehaus.plexus.util.io.CachingOutputStream;
044import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
045import org.codehaus.plexus.util.xml.XMLWriter;
046import org.slf4j.Logger;
047import org.slf4j.LoggerFactory;
048
049import static java.nio.charset.StandardCharsets.UTF_8;
050
051/**
052 * Serializes
053 * <ol>
054 * <li>a standard <a href="/ref/current/maven-plugin-api/plugin.html">Maven Plugin Descriptor XML file</a></li>
055 * <li>a descriptor containing a limited set of elements for {@link PluginHelpGenerator}</li>
056 * <li>an enhanced descriptor containing HTML values for some elements (instead of plain text as for the other two)
057 * for {@code org.apache.maven.plugin.plugin.report.GoalRenderer}</li>
058 * </ol>
059 * from a given in-memory descriptor. The in-memory descriptor acting as source is supposed to contain XHTML values
060 * for description elements.
061 *
062 */
063public class PluginDescriptorFilesGenerator implements Generator {
064    private static final Logger LOG = LoggerFactory.getLogger(PluginDescriptorFilesGenerator.class);
065
066    /**
067     * The type of the plugin descriptor file
068     */
069    enum DescriptorType {
070        STANDARD,
071        LIMITED_FOR_HELP_MOJO,
072        XHTML
073    }
074
075    @Override
076    public void execute(File destinationDirectory, PluginToolsRequest request) throws GeneratorException {
077        try {
078            // write standard plugin.xml descriptor
079            File f = new File(destinationDirectory, "plugin.xml");
080            writeDescriptor(f, request, DescriptorType.STANDARD);
081
082            // write plugin-help.xml help-descriptor (containing only a limited set of attributes)
083            MavenProject mavenProject = request.getProject();
084            f = new File(destinationDirectory, PluginHelpGenerator.getPluginHelpPath(mavenProject));
085            writeDescriptor(f, request, DescriptorType.LIMITED_FOR_HELP_MOJO);
086
087            // write enhanced plugin-enhanced.xml descriptor (containing some XHTML values)
088            f = getEnhancedDescriptorFilePath(mavenProject);
089            writeDescriptor(f, request, DescriptorType.XHTML);
090        } catch (IOException e) {
091            throw new GeneratorException(e.getMessage(), e);
092        }
093    }
094
095    public static File getEnhancedDescriptorFilePath(MavenProject project) {
096        return new File(project.getBuild().getDirectory(), "plugin-enhanced.xml");
097    }
098
099    private String getVersion() {
100        Package p = this.getClass().getPackage();
101        String version = (p == null) ? null : p.getSpecificationVersion();
102        return (version == null) ? "SNAPSHOT" : version;
103    }
104
105    public void writeDescriptor(File destinationFile, PluginToolsRequest request, DescriptorType type)
106            throws IOException {
107        PluginDescriptor pluginDescriptor = request.getPluginDescriptor();
108
109        if (!destinationFile.getParentFile().exists()) {
110            destinationFile.getParentFile().mkdirs();
111        }
112
113        try (Writer writer = new OutputStreamWriter(new CachingOutputStream(destinationFile), UTF_8)) {
114            XMLWriter w = new PrettyPrintXMLWriter(writer, UTF_8.name(), null);
115
116            final String additionalInfo;
117            switch (type) {
118                case LIMITED_FOR_HELP_MOJO:
119                    additionalInfo = " (for help mojo with limited elements)";
120                    break;
121                case XHTML:
122                    additionalInfo = " (enhanced XHTML version (used for plugin:report))";
123                    break;
124                default:
125                    additionalInfo = "";
126                    break;
127            }
128            w.writeMarkup("\n<!-- Generated by maven-plugin-tools " + getVersion() + additionalInfo + "-->\n\n");
129
130            w.startElement("plugin");
131
132            GeneratorUtils.element(w, "name", pluginDescriptor.getName());
133
134            GeneratorUtils.element(w, "description", pluginDescriptor.getDescription());
135
136            GeneratorUtils.element(w, "groupId", pluginDescriptor.getGroupId());
137
138            GeneratorUtils.element(w, "artifactId", pluginDescriptor.getArtifactId());
139
140            GeneratorUtils.element(w, "version", pluginDescriptor.getVersion());
141
142            GeneratorUtils.element(w, "goalPrefix", pluginDescriptor.getGoalPrefix());
143
144            if (type != DescriptorType.LIMITED_FOR_HELP_MOJO) {
145                GeneratorUtils.element(w, "isolatedRealm", String.valueOf(pluginDescriptor.isIsolatedRealm()));
146
147                GeneratorUtils.element(
148                        w, "inheritedByDefault", String.valueOf(pluginDescriptor.isInheritedByDefault()));
149
150                if (pluginDescriptor instanceof ExtendedPluginDescriptor) {
151                    ExtendedPluginDescriptor extPluginDescriptor = (ExtendedPluginDescriptor) pluginDescriptor;
152                    if (StringUtils.isNotBlank(extPluginDescriptor.getRequiredJavaVersion())) {
153                        GeneratorUtils.element(w, "requiredJavaVersion", extPluginDescriptor.getRequiredJavaVersion());
154                    }
155                }
156                if (StringUtils.isNotBlank(pluginDescriptor.getRequiredMavenVersion())) {
157                    GeneratorUtils.element(w, "requiredMavenVersion", pluginDescriptor.getRequiredMavenVersion());
158                }
159            }
160
161            w.startElement("mojos");
162
163            final JavadocLinkGenerator javadocLinkGenerator;
164            if (request.getInternalJavadocBaseUrl() != null
165                    || (request.getExternalJavadocBaseUrls() != null
166                            && !request.getExternalJavadocBaseUrls().isEmpty())) {
167                javadocLinkGenerator = new JavadocLinkGenerator(
168                        request.getInternalJavadocBaseUrl(),
169                        request.getInternalJavadocVersion(),
170                        request.getExternalJavadocBaseUrls(),
171                        request.getSettings());
172            } else {
173                javadocLinkGenerator = null;
174            }
175            if (pluginDescriptor.getMojos() != null) {
176                List<MojoDescriptor> descriptors = pluginDescriptor.getMojos();
177
178                PluginUtils.sortMojos(descriptors);
179
180                for (MojoDescriptor descriptor : descriptors) {
181                    processMojoDescriptor(descriptor, w, type, javadocLinkGenerator);
182                }
183            }
184
185            w.endElement();
186
187            if (type != DescriptorType.LIMITED_FOR_HELP_MOJO) {
188                GeneratorUtils.writeDependencies(w, pluginDescriptor);
189            }
190
191            w.endElement();
192
193            writer.flush();
194        }
195    }
196
197    /**
198     *
199     * @param type
200     * @param containsXhtmlValue
201     * @param text
202     * @return the normalized text value (i.e. potentially converted to XHTML)
203     */
204    private static String getTextValue(DescriptorType type, boolean containsXhtmlValue, String text) {
205        final String xhtmlText;
206        if (!containsXhtmlValue) // text comes from legacy extractor
207        {
208            xhtmlText = GeneratorUtils.makeHtmlValid(text);
209        } else {
210            xhtmlText = text;
211        }
212        if (type != DescriptorType.XHTML) {
213            return new HtmlToPlainTextConverter().convert(text);
214        } else {
215            return xhtmlText;
216        }
217    }
218
219    @SuppressWarnings("deprecation")
220    protected void processMojoDescriptor(
221            MojoDescriptor mojoDescriptor,
222            XMLWriter w,
223            DescriptorType type,
224            JavadocLinkGenerator javadocLinkGenerator) {
225        boolean containsXhtmlTextValues = mojoDescriptor instanceof ExtendedMojoDescriptor
226                && ((ExtendedMojoDescriptor) mojoDescriptor).containsXhtmlTextValues();
227
228        w.startElement("mojo");
229
230        // ----------------------------------------------------------------------
231        //
232        // ----------------------------------------------------------------------
233
234        w.startElement("goal");
235        w.writeText(mojoDescriptor.getGoal());
236        w.endElement();
237
238        // ----------------------------------------------------------------------
239        //
240        // ----------------------------------------------------------------------
241
242        String description = mojoDescriptor.getDescription();
243
244        if (description != null && !description.isEmpty()) {
245            w.startElement("description");
246            w.writeText(getTextValue(type, containsXhtmlTextValues, mojoDescriptor.getDescription()));
247            w.endElement();
248        }
249
250        // ----------------------------------------------------------------------
251        //
252        // ----------------------------------------------------------------------
253
254        if (StringUtils.isNotEmpty(mojoDescriptor.isDependencyResolutionRequired())) {
255            GeneratorUtils.element(w, "requiresDependencyResolution", mojoDescriptor.isDependencyResolutionRequired());
256        }
257
258        // ----------------------------------------------------------------------
259        //
260        // ----------------------------------------------------------------------
261
262        GeneratorUtils.element(w, "requiresDirectInvocation", String.valueOf(mojoDescriptor.isDirectInvocationOnly()));
263
264        // ----------------------------------------------------------------------
265        //
266        // ----------------------------------------------------------------------
267
268        GeneratorUtils.element(w, "requiresProject", String.valueOf(mojoDescriptor.isProjectRequired()));
269
270        // ----------------------------------------------------------------------
271        //
272        // ----------------------------------------------------------------------
273
274        GeneratorUtils.element(w, "requiresReports", String.valueOf(mojoDescriptor.isRequiresReports()));
275
276        // ----------------------------------------------------------------------
277        //
278        // ----------------------------------------------------------------------
279
280        GeneratorUtils.element(w, "aggregator", String.valueOf(mojoDescriptor.isAggregator()));
281
282        // ----------------------------------------------------------------------
283        //
284        // ----------------------------------------------------------------------
285
286        GeneratorUtils.element(w, "requiresOnline", String.valueOf(mojoDescriptor.isOnlineRequired()));
287
288        // ----------------------------------------------------------------------
289        //
290        // ----------------------------------------------------------------------
291
292        GeneratorUtils.element(w, "inheritedByDefault", String.valueOf(mojoDescriptor.isInheritedByDefault()));
293
294        // ----------------------------------------------------------------------
295        //
296        // ----------------------------------------------------------------------
297
298        if (StringUtils.isNotEmpty(mojoDescriptor.getPhase())) {
299            GeneratorUtils.element(w, "phase", mojoDescriptor.getPhase());
300        }
301
302        // ----------------------------------------------------------------------
303        //
304        // ----------------------------------------------------------------------
305
306        if (StringUtils.isNotEmpty(mojoDescriptor.getExecutePhase())) {
307            GeneratorUtils.element(w, "executePhase", mojoDescriptor.getExecutePhase());
308        }
309
310        if (StringUtils.isNotEmpty(mojoDescriptor.getExecuteGoal())) {
311            GeneratorUtils.element(w, "executeGoal", mojoDescriptor.getExecuteGoal());
312        }
313
314        if (StringUtils.isNotEmpty(mojoDescriptor.getExecuteLifecycle())) {
315            GeneratorUtils.element(w, "executeLifecycle", mojoDescriptor.getExecuteLifecycle());
316        }
317
318        // ----------------------------------------------------------------------
319        //
320        // ----------------------------------------------------------------------
321
322        w.startElement("implementation");
323        w.writeText(mojoDescriptor.getImplementation());
324        w.endElement();
325
326        // ----------------------------------------------------------------------
327        //
328        // ----------------------------------------------------------------------
329
330        w.startElement("language");
331        w.writeText(mojoDescriptor.getLanguage());
332        w.endElement();
333
334        // ----------------------------------------------------------------------
335        //
336        // ----------------------------------------------------------------------
337
338        if (StringUtils.isNotEmpty(mojoDescriptor.getComponentConfigurator())) {
339            w.startElement("configurator");
340            w.writeText(mojoDescriptor.getComponentConfigurator());
341            w.endElement();
342        }
343
344        // ----------------------------------------------------------------------
345        //
346        // ----------------------------------------------------------------------
347
348        if (StringUtils.isNotEmpty(mojoDescriptor.getComponentComposer())) {
349            w.startElement("composer");
350            w.writeText(mojoDescriptor.getComponentComposer());
351            w.endElement();
352        }
353
354        // ----------------------------------------------------------------------
355        //
356        // ----------------------------------------------------------------------
357
358        w.startElement("instantiationStrategy");
359        w.writeText(mojoDescriptor.getInstantiationStrategy());
360        w.endElement();
361
362        // ----------------------------------------------------------------------
363        // Strategy for handling repeated reference to mojo in
364        // the calculated (decorated, resolved) execution stack
365        // ----------------------------------------------------------------------
366        w.startElement("executionStrategy");
367        w.writeText(mojoDescriptor.getExecutionStrategy());
368        w.endElement();
369
370        // ----------------------------------------------------------------------
371        //
372        // ----------------------------------------------------------------------
373
374        if (mojoDescriptor.getSince() != null) {
375            w.startElement("since");
376
377            if (StringUtils.isEmpty(mojoDescriptor.getSince())) {
378                w.writeText("No version given");
379            } else {
380                w.writeText(mojoDescriptor.getSince());
381            }
382
383            w.endElement();
384        }
385
386        // ----------------------------------------------------------------------
387        //
388        // ----------------------------------------------------------------------
389
390        if (mojoDescriptor.getDeprecated() != null) {
391            w.startElement("deprecated");
392
393            if (StringUtils.isEmpty(mojoDescriptor.getDeprecated())) {
394                w.writeText("No reason given");
395            } else {
396                w.writeText(getTextValue(type, containsXhtmlTextValues, mojoDescriptor.getDeprecated()));
397            }
398
399            w.endElement();
400        }
401
402        // ----------------------------------------------------------------------
403        // Extended (3.0) descriptor
404        // ----------------------------------------------------------------------
405
406        if (mojoDescriptor instanceof ExtendedMojoDescriptor) {
407            ExtendedMojoDescriptor extendedMojoDescriptor = (ExtendedMojoDescriptor) mojoDescriptor;
408            if (extendedMojoDescriptor.getDependencyCollectionRequired() != null) {
409                GeneratorUtils.element(
410                        w, "requiresDependencyCollection", extendedMojoDescriptor.getDependencyCollectionRequired());
411            }
412
413            GeneratorUtils.element(w, "threadSafe", String.valueOf(extendedMojoDescriptor.isThreadSafe()));
414
415            boolean v4Api = extendedMojoDescriptor.isV4Api();
416            if (v4Api) {
417                GeneratorUtils.element(w, "v4Api", String.valueOf(v4Api));
418            }
419        }
420
421        // ----------------------------------------------------------------------
422        // Parameters
423        // ----------------------------------------------------------------------
424
425        List<Parameter> parameters = mojoDescriptor.getParameters();
426
427        w.startElement("parameters");
428
429        Map<String, Requirement> requirements = new LinkedHashMap<>();
430
431        Set<Parameter> configuration = new LinkedHashSet<>();
432
433        if (parameters != null) {
434            if (type == DescriptorType.LIMITED_FOR_HELP_MOJO) {
435                PluginUtils.sortMojoParameters(parameters);
436            }
437
438            for (Parameter parameter : parameters) {
439                String expression = getExpression(parameter);
440
441                if ((expression != null && !expression.isEmpty()) && expression.startsWith("${component.")) {
442                    // treat it as a component...a requirement, in other words.
443
444                    // remove "component." plus expression delimiters
445                    String role = expression.substring("${component.".length(), expression.length() - 1);
446
447                    String roleHint = null;
448
449                    int posRoleHintSeparator = role.indexOf('#');
450                    if (posRoleHintSeparator > 0) {
451                        roleHint = role.substring(posRoleHintSeparator + 1);
452
453                        role = role.substring(0, posRoleHintSeparator);
454                    }
455
456                    // TODO: remove deprecated expression
457                    requirements.put(parameter.getName(), new Requirement(role, roleHint));
458                } else if (parameter.getRequirement() != null) {
459                    requirements.put(parameter.getName(), parameter.getRequirement());
460                }
461                // don't show readonly parameters in help
462                else if (type != DescriptorType.LIMITED_FOR_HELP_MOJO || parameter.isEditable()) {
463                    // treat it as a normal parameter.
464
465                    w.startElement("parameter");
466
467                    GeneratorUtils.element(w, "name", parameter.getName());
468
469                    if (parameter.getAlias() != null) {
470                        GeneratorUtils.element(w, "alias", parameter.getAlias());
471                    }
472
473                    writeParameterType(w, type, javadocLinkGenerator, parameter, mojoDescriptor.getGoal());
474
475                    if (parameter.getSince() != null) {
476                        w.startElement("since");
477
478                        if (StringUtils.isEmpty(parameter.getSince())) {
479                            w.writeText("No version given");
480                        } else {
481                            w.writeText(parameter.getSince());
482                        }
483
484                        w.endElement();
485                    }
486
487                    if (parameter.getDeprecated() != null) {
488                        if (StringUtils.isEmpty(parameter.getDeprecated())) {
489                            GeneratorUtils.element(w, "deprecated", "No reason given");
490                        } else {
491                            GeneratorUtils.element(
492                                    w,
493                                    "deprecated",
494                                    getTextValue(type, containsXhtmlTextValues, parameter.getDeprecated()));
495                        }
496                    }
497
498                    if (parameter.getImplementation() != null) {
499                        GeneratorUtils.element(w, "implementation", parameter.getImplementation());
500                    }
501
502                    GeneratorUtils.element(w, "required", Boolean.toString(parameter.isRequired()));
503
504                    GeneratorUtils.element(w, "editable", Boolean.toString(parameter.isEditable()));
505
506                    GeneratorUtils.element(
507                            w, "description", getTextValue(type, containsXhtmlTextValues, parameter.getDescription()));
508
509                    if (StringUtils.isNotEmpty(parameter.getDefaultValue())
510                            || StringUtils.isNotEmpty(parameter.getExpression())) {
511                        configuration.add(parameter);
512                    }
513
514                    w.endElement();
515                }
516            }
517        }
518
519        w.endElement();
520
521        // ----------------------------------------------------------------------
522        // Configuration
523        // ----------------------------------------------------------------------
524
525        if (!configuration.isEmpty()) {
526            w.startElement("configuration");
527
528            for (Parameter parameter : configuration) {
529                if (type == DescriptorType.LIMITED_FOR_HELP_MOJO && !parameter.isEditable()) {
530                    // don't show readonly parameters in help
531                    continue;
532                }
533
534                w.startElement(parameter.getName());
535
536                // strip type by parameter type (generics) information
537                String parameterType = StringUtils.chomp(parameter.getType(), "<");
538                if (parameterType != null && !parameterType.isEmpty()) {
539                    w.addAttribute("implementation", parameterType);
540                }
541
542                if (parameter.getDefaultValue() != null) {
543                    w.addAttribute("default-value", parameter.getDefaultValue());
544                }
545
546                if (StringUtils.isNotEmpty(parameter.getExpression())) {
547                    w.writeText(parameter.getExpression());
548                }
549
550                w.endElement();
551            }
552
553            w.endElement();
554        }
555
556        // ----------------------------------------------------------------------
557        // Requirements
558        // ----------------------------------------------------------------------
559
560        if (!requirements.isEmpty() && type != DescriptorType.LIMITED_FOR_HELP_MOJO) {
561            w.startElement("requirements");
562
563            for (Map.Entry<String, Requirement> entry : requirements.entrySet()) {
564                String key = entry.getKey();
565                Requirement requirement = entry.getValue();
566
567                w.startElement("requirement");
568
569                GeneratorUtils.element(w, "role", requirement.getRole());
570
571                if (StringUtils.isNotEmpty(requirement.getRoleHint())) {
572                    GeneratorUtils.element(w, "role-hint", requirement.getRoleHint());
573                }
574
575                GeneratorUtils.element(w, "field-name", key);
576
577                w.endElement();
578            }
579
580            w.endElement();
581        }
582
583        w.endElement();
584    }
585
586    /**
587     * Writes parameter type information and potentially also the related javadoc URL.
588     * @param w
589     * @param type
590     * @param javadocLinkGenerator
591     * @param parameter
592     * @param goal
593     */
594    protected void writeParameterType(
595            XMLWriter w,
596            DescriptorType type,
597            JavadocLinkGenerator javadocLinkGenerator,
598            Parameter parameter,
599            String goal) {
600        String parameterType = parameter.getType();
601
602        if (type == DescriptorType.STANDARD) {
603            // strip type by parameter type (generics) information for standard plugin descriptor
604            parameterType = StringUtils.chomp(parameterType, "<");
605        }
606        GeneratorUtils.element(w, "type", parameterType);
607
608        if (type == DescriptorType.XHTML && javadocLinkGenerator != null) {
609            // skip primitives which never has javadoc
610            if (parameter.getType().indexOf('.') == -1) {
611                LOG.debug("Javadoc URLs are not available for primitive types like {}", parameter.getType());
612            } else {
613                try {
614                    URI javadocUrl = getJavadocUrlForType(javadocLinkGenerator, parameterType);
615                    GeneratorUtils.element(w, "typeJavadocUrl", javadocUrl.toString());
616                } catch (IllegalArgumentException e) {
617                    LOG.warn(
618                            "Could not get javadoc URL for type {} of parameter {} from goal {}: {}",
619                            parameter.getType(),
620                            parameter.getName(),
621                            goal,
622                            e.getMessage());
623                }
624            }
625        }
626    }
627
628    private static String extractBinaryNameForJavadoc(String type) {
629        final String binaryName;
630        int startOfParameterType = type.indexOf("<");
631        if (startOfParameterType != -1) {
632            // parse parameter type
633            String mainType = type.substring(0, startOfParameterType);
634
635            // some heuristics here
636            String[] parameterTypes = type.substring(startOfParameterType + 1, type.lastIndexOf(">"))
637                    .split(",\\s*");
638            switch (parameterTypes.length) {
639                case 1: // if only one parameter type, assume collection, first parameter type is most interesting
640                    binaryName = extractBinaryNameForJavadoc(parameterTypes[0]);
641                    break;
642                case 2: // if two parameter types assume map, second parameter type is most interesting
643                    binaryName = extractBinaryNameForJavadoc(parameterTypes[1]);
644                    break;
645                default:
646                    // all other cases link to main type
647                    binaryName = mainType;
648            }
649        } else {
650            binaryName = type;
651        }
652        return binaryName;
653    }
654
655    static URI getJavadocUrlForType(JavadocLinkGenerator javadocLinkGenerator, String type) {
656        return javadocLinkGenerator.createLink(extractBinaryNameForJavadoc(type));
657    }
658
659    /**
660     * Get the expression value, eventually surrounding it with <code>${ }</code>.
661     *
662     * @param parameter the parameter
663     * @return the expression value
664     */
665    private String getExpression(Parameter parameter) {
666        String expression = parameter.getExpression();
667        if (StringUtils.isNotBlank(expression) && !expression.contains("${")) {
668            expression = "${" + expression.trim() + "}";
669            parameter.setExpression(expression);
670        }
671        return expression;
672    }
673}