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.io.IOException;
023import java.io.Reader;
024import java.nio.file.Files;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.List;
028import java.util.Locale;
029import java.util.stream.Collectors;
030
031import org.apache.maven.RepositoryUtils;
032import org.apache.maven.artifact.ArtifactUtils;
033import org.apache.maven.doxia.sink.Sink;
034import org.apache.maven.execution.MavenSession;
035import org.apache.maven.model.building.ModelBuildingRequest;
036import org.apache.maven.plugin.descriptor.MojoDescriptor;
037import org.apache.maven.plugin.descriptor.PluginDescriptor;
038import org.apache.maven.plugin.descriptor.PluginDescriptorBuilder;
039import org.apache.maven.plugins.annotations.Component;
040import org.apache.maven.plugins.annotations.Execute;
041import org.apache.maven.plugins.annotations.LifecyclePhase;
042import org.apache.maven.plugins.annotations.Mojo;
043import org.apache.maven.plugins.annotations.Parameter;
044import org.apache.maven.plugins.plugin.descriptor.EnhancedPluginDescriptorBuilder;
045import org.apache.maven.project.DefaultProjectBuildingRequest;
046import org.apache.maven.project.MavenProject;
047import org.apache.maven.project.ProjectBuilder;
048import org.apache.maven.project.ProjectBuildingException;
049import org.apache.maven.project.ProjectBuildingRequest;
050import org.apache.maven.reporting.AbstractMavenReport;
051import org.apache.maven.reporting.MavenReportException;
052import org.apache.maven.rtinfo.RuntimeInformation;
053import org.codehaus.plexus.configuration.PlexusConfigurationException;
054import org.codehaus.plexus.i18n.I18N;
055import org.codehaus.plexus.util.xml.XmlStreamReader;
056import org.eclipse.aether.RepositorySystem;
057import org.eclipse.aether.artifact.DefaultArtifact;
058import org.eclipse.aether.resolution.VersionRangeRequest;
059import org.eclipse.aether.resolution.VersionRangeResolutionException;
060import org.eclipse.aether.resolution.VersionRangeResult;
061import org.eclipse.aether.version.Version;
062
063/**
064 * Generates the Plugin's documentation report: <code>plugin-info.html</code> plugin overview page,
065 * and one <code><i>goal</i>-mojo.html</code> per goal.
066 * Relies on one output file from <a href="../maven-plugin-plugin/descriptor-mojo.html">plugin:descriptor</a>.
067 *
068 * @author <a href="snicoll@apache.org">Stephane Nicoll</a>
069 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
070 * @since 3.7.0
071 */
072@Mojo(name = "report", threadSafe = true)
073@Execute(phase = LifecyclePhase.PROCESS_CLASSES)
074public class PluginReport extends AbstractMavenReport {
075
076    /**
077     * Set this to "true" to skip generating the report.
078     *
079     * @since 3.7.0
080     */
081    @Parameter(defaultValue = "false", property = "maven.plugin.report.skip")
082    private boolean skip;
083
084    /**
085     * Set this to "true" to generate the usage section for "plugin-info.html" with
086     * {@code <extensions>true</extensions>}.
087     *
088     * @since 3.7.0
089     */
090    @Parameter(defaultValue = "false", property = "maven.plugin.report.hasExtensionsToLoad")
091    private boolean hasExtensionsToLoad;
092
093    /**
094     * The Plugin requirements history list.
095     * <p>
096     * Can be specified as list of <code>requirementsHistory</code>:
097     *
098     * <pre>
099     * &lt;requirementsHistories&gt;
100     *   &lt;requirementsHistory&gt;
101     *     &lt;version&gt;plugin version&lt;/version&gt;
102     *     &lt;maven&gt;maven version&lt;/maven&gt;
103     *     &lt;jdk&gt;jdk version&lt;/jdk&gt;
104     *   &lt;/requirementsHistory&gt;
105     * &lt;/requirementsHistories&gt;
106     * </pre>
107     *
108     * @since 3.7.0
109     */
110    @Parameter
111    private List<RequirementsHistory> requirementsHistories = new ArrayList<>();
112
113    /**
114     * Plugin's version range for automatic detection of requirements history.
115     *
116     * @since 3.12.0
117     */
118    @Parameter(defaultValue = "[0,)")
119    private String requirementsHistoryDetectionRange;
120
121    @Component
122    private RuntimeInformation rtInfo;
123
124    /**
125     * Internationalization component.
126     */
127    @Component
128    private I18N i18n;
129
130    /**
131     * Path to enhanced plugin descriptor to generate the report from (must contain some XHTML values)
132     *
133     * @since 3.7.0
134     */
135    @Parameter(defaultValue = "${project.build.directory}/plugin-enhanced.xml", required = true, readonly = true)
136    private File enhancedPluginXmlFile;
137
138    /**
139     * In case the internal javadoc site has not been generated when running this report goal
140     * (e.g. when using an aggregator javadoc report) link validation needs to be disabled by setting
141     * this value to {@code true}.
142     * This might have the drawback that some links being generated in the report might be broken
143     * in case not all parameter types and javadoc link references are resolvable through the sites being given to
144     * goal {@code plugin:descriptor}.
145     *
146     * @since 3.7.0
147     */
148    @Parameter(property = "maven.plugin.report.disableInternalJavadocLinkValidation")
149    private boolean disableInternalJavadocLinkValidation;
150
151    @Component
152    private MavenSession mavenSession;
153
154    @Component
155    private RepositorySystem repositorySystem;
156
157    @Component
158    private ProjectBuilder projectBuilder;
159
160    /**
161     * {@inheritDoc}
162     */
163    @Override
164    public boolean canGenerateReport() {
165        if (skip) {
166            getLog().info("Maven Plugin Plugin Report generation skipped.");
167            return false;
168        }
169
170        if (!(enhancedPluginXmlFile != null && enhancedPluginXmlFile.isFile() && enhancedPluginXmlFile.canRead())) {
171            return false;
172        }
173
174        return true;
175    }
176
177    /**
178     * {@inheritDoc}
179     */
180    @Override
181    protected void executeReport(Locale locale) throws MavenReportException {
182        PluginDescriptor pluginDescriptor = extractPluginDescriptor();
183
184        // Generate the mojos' documentation
185        generateMojosDocumentation(pluginDescriptor, locale);
186
187        if (requirementsHistories.isEmpty()) {
188            // detect requirements history
189            String v = null;
190            try {
191                List<Version> versions = discoverVersions(requirementsHistoryDetectionRange);
192                if (versions.isEmpty()) {
193                    getLog().info("No plugin history found for range " + requirementsHistoryDetectionRange);
194                } else {
195                    getLog().info("Detecting plugin requirements history for range "
196                            + requirementsHistoryDetectionRange + ": "
197                            + versions.size() + " releases, from " + versions.get(0) + " to "
198                            + versions.get(versions.size() - 1));
199                }
200
201                Collections.reverse(versions);
202                for (Version version : versions) {
203                    v = version.toString();
204                    MavenProject versionProject = buildMavenProject(v);
205                    RequirementsHistory requirements = RequirementsHistory.discoverRequirements(versionProject);
206                    requirementsHistories.add(requirements);
207                    getLog().info("  - " + requirements);
208                }
209            } catch (VersionRangeResolutionException vrre) {
210                throw new MavenReportException(
211                        "Cannot resolve past versions " + requirementsHistoryDetectionRange, vrre);
212            } catch (ProjectBuildingException pbe) {
213                throw new MavenReportException("Cannot resolve MavenProject for version " + v, pbe);
214            }
215        }
216
217        // Write the overview
218        PluginOverviewRenderer r = new PluginOverviewRenderer(
219                getSink(), i18n, locale, getProject(), requirementsHistories, pluginDescriptor, hasExtensionsToLoad);
220        r.render();
221    }
222
223    private PluginDescriptor extractPluginDescriptor() throws MavenReportException {
224        PluginDescriptorBuilder builder = new EnhancedPluginDescriptorBuilder(rtInfo);
225
226        try (Reader input = new XmlStreamReader(Files.newInputStream(enhancedPluginXmlFile.toPath()))) {
227            return builder.build(input);
228        } catch (IOException | PlexusConfigurationException e) {
229            throw new MavenReportException("Error extracting plugin descriptor from " + enhancedPluginXmlFile, e);
230        }
231    }
232
233    /**
234     * @param locale The locale
235     * @param key The key to search for
236     * @return The text appropriate for the locale.
237     */
238    private String getI18nString(Locale locale, String key) {
239        return i18n.getString("plugin-report", locale, "report.plugin." + key);
240    }
241
242    /**
243     * {@inheritDoc}
244     */
245    @Override
246    public String getName(Locale locale) {
247        return getI18nString(locale, "name");
248    }
249
250    /**
251     * {@inheritDoc}
252     */
253    @Override
254    public String getDescription(Locale locale) {
255        return getI18nString(locale, "description");
256    }
257
258    /**
259     * {@inheritDoc}
260     */
261    @Override
262    public String getOutputName() {
263        return "plugin-info";
264    }
265
266    /**
267     * Generate the mojos' documentation with the {@link #getSinkFactory()}
268     *
269     * @param pluginDescriptor not null
270     * @param locale           not null
271     * @throws MavenReportException if any
272     * @throws IOException
273     */
274    private void generateMojosDocumentation(PluginDescriptor pluginDescriptor, Locale locale)
275            throws MavenReportException {
276
277        if (pluginDescriptor.getMojos() != null) {
278            for (MojoDescriptor descriptor : pluginDescriptor.getMojos()) {
279                GoalRenderer renderer;
280                try {
281                    String filename = descriptor.getGoal() + "-mojo.html";
282                    Sink sink = getSinkFactory().createSink(getReportOutputDirectory(), filename);
283                    renderer = new GoalRenderer(
284                            sink,
285                            i18n,
286                            locale,
287                            project,
288                            descriptor,
289                            getReportOutputDirectory(),
290                            disableInternalJavadocLinkValidation,
291                            getLog());
292                } catch (IOException e) {
293                    throw new MavenReportException("Cannot generate sink for mojo " + descriptor.getGoal(), e);
294                }
295                renderer.render();
296            }
297        }
298    }
299
300    private List<Version> discoverVersions(String range) throws VersionRangeResolutionException {
301        MavenProject currentProject = mavenSession.getCurrentProject();
302        VersionRangeRequest rangeRequest = new VersionRangeRequest();
303        rangeRequest.setArtifact(
304                new DefaultArtifact(currentProject.getGroupId() + ":" + currentProject.getArtifactId() + ":" + range));
305        rangeRequest.setRepositories(
306                RepositoryUtils.toRepos(mavenSession.getCurrentProject().getRemoteArtifactRepositories()));
307        VersionRangeResult rangeResult =
308                repositorySystem.resolveVersionRange(mavenSession.getRepositorySession(), rangeRequest);
309        return rangeResult.getVersions().stream()
310                .filter(version -> !ArtifactUtils.isSnapshot(version.toString()))
311                .collect(Collectors.toList());
312    }
313
314    private MavenProject buildMavenProject(String version) throws ProjectBuildingException {
315        MavenProject currentProject = mavenSession.getCurrentProject();
316        ProjectBuildingRequest buildRequest = new DefaultProjectBuildingRequest();
317        buildRequest.setLocalRepository(mavenSession.getLocalRepository());
318        buildRequest.setRemoteRepositories(mavenSession.getCurrentProject().getRemoteArtifactRepositories());
319        buildRequest.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL);
320        buildRequest.setProcessPlugins(false);
321        buildRequest.setRepositoryMerging(ProjectBuildingRequest.RepositoryMerging.REQUEST_DOMINANT);
322        buildRequest.setSystemProperties(mavenSession.getSystemProperties());
323        buildRequest.setUserProperties(mavenSession.getUserProperties());
324        buildRequest.setRepositorySession(mavenSession.getRepositorySession());
325        return projectBuilder
326                .build(
327                        RepositoryUtils.toArtifact(new DefaultArtifact(currentProject.getGroupId() + ":"
328                                + currentProject.getArtifactId() + ":pom:" + version)),
329                        buildRequest)
330                .getProject();
331    }
332}