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