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            return false;
167        }
168
169        if (!(enhancedPluginXmlFile != null && enhancedPluginXmlFile.isFile() && enhancedPluginXmlFile.canRead())) {
170            return false;
171        }
172
173        return true;
174    }
175
176    /**
177     * {@inheritDoc}
178     */
179    @Override
180    protected void executeReport(Locale locale) throws MavenReportException {
181        PluginDescriptor pluginDescriptor = extractPluginDescriptor();
182
183        // Generate the mojos' documentation
184        generateMojosDocumentation(pluginDescriptor, locale);
185
186        if (requirementsHistories.isEmpty()) {
187            // detect requirements history
188            String v = null;
189            try {
190                List<Version> versions = discoverVersions(requirementsHistoryDetectionRange);
191                if (versions.isEmpty()) {
192                    getLog().info("No plugin history found for range " + requirementsHistoryDetectionRange);
193                } else {
194                    getLog().info("Detecting plugin requirements history for range "
195                            + requirementsHistoryDetectionRange + ": "
196                            + versions.size() + " releases, from " + versions.get(0) + " to "
197                            + versions.get(versions.size() - 1));
198                }
199
200                Collections.reverse(versions);
201                for (Version version : versions) {
202                    v = version.toString();
203                    MavenProject versionProject = buildMavenProject(v);
204                    RequirementsHistory requirements = RequirementsHistory.discoverRequirements(versionProject);
205                    requirementsHistories.add(requirements);
206                    getLog().debug("  - " + requirements);
207                }
208            } catch (VersionRangeResolutionException vrre) {
209                throw new MavenReportException(
210                        "Cannot resolve past versions " + requirementsHistoryDetectionRange, vrre);
211            } catch (ProjectBuildingException pbe) {
212                throw new MavenReportException("Cannot resolve MavenProject for version " + v, pbe);
213            }
214        }
215
216        // Write the overview
217        PluginOverviewRenderer r = new PluginOverviewRenderer(
218                getSink(), i18n, locale, getProject(), requirementsHistories, pluginDescriptor, hasExtensionsToLoad);
219        r.render();
220    }
221
222    private PluginDescriptor extractPluginDescriptor() throws MavenReportException {
223        PluginDescriptorBuilder builder = new EnhancedPluginDescriptorBuilder(rtInfo);
224
225        try (Reader input = new XmlStreamReader(Files.newInputStream(enhancedPluginXmlFile.toPath()))) {
226            return builder.build(input);
227        } catch (IOException | PlexusConfigurationException e) {
228            throw new MavenReportException("Error extracting plugin descriptor from " + enhancedPluginXmlFile, e);
229        }
230    }
231
232    /**
233     * @param locale The locale
234     * @param key The key to search for
235     * @return The text appropriate for the locale.
236     */
237    private String getI18nString(Locale locale, String key) {
238        return i18n.getString("plugin-report", locale, "report.plugin." + key);
239    }
240
241    /**
242     * {@inheritDoc}
243     */
244    @Override
245    public String getName(Locale locale) {
246        return getI18nString(locale, "name");
247    }
248
249    /**
250     * {@inheritDoc}
251     */
252    @Override
253    public String getDescription(Locale locale) {
254        return getI18nString(locale, "description");
255    }
256
257    /**
258     * {@inheritDoc}
259     */
260    @Override
261    public String getOutputName() {
262        return "plugin-info";
263    }
264
265    /**
266     * Generate the mojos' documentation with the {@link #getSinkFactory()}
267     *
268     * @param pluginDescriptor not null
269     * @param locale           not null
270     * @throws MavenReportException if any
271     * @throws IOException
272     */
273    private void generateMojosDocumentation(PluginDescriptor pluginDescriptor, Locale locale)
274            throws MavenReportException {
275        if (pluginDescriptor.getMojos() != null) {
276            for (MojoDescriptor descriptor : pluginDescriptor.getMojos()) {
277                GoalRenderer renderer;
278                try {
279                    String filename = descriptor.getGoal() + "-mojo.html";
280                    Sink sink = getSinkFactory().createSink(getReportOutputDirectory(), filename);
281                    renderer = new GoalRenderer(
282                            sink,
283                            i18n,
284                            locale,
285                            project,
286                            descriptor,
287                            getReportOutputDirectory(),
288                            disableInternalJavadocLinkValidation,
289                            getLog());
290                } catch (IOException e) {
291                    throw new MavenReportException("Cannot generate sink for mojo " + descriptor.getGoal(), e);
292                }
293                renderer.render();
294            }
295        }
296    }
297
298    private List<Version> discoverVersions(String range) throws VersionRangeResolutionException {
299        MavenProject currentProject = mavenSession.getCurrentProject();
300        VersionRangeRequest rangeRequest = new VersionRangeRequest();
301        rangeRequest.setArtifact(
302                new DefaultArtifact(currentProject.getGroupId() + ":" + currentProject.getArtifactId() + ":" + range));
303        rangeRequest.setRepositories(
304                RepositoryUtils.toRepos(mavenSession.getCurrentProject().getRemoteArtifactRepositories()));
305        VersionRangeResult rangeResult =
306                repositorySystem.resolveVersionRange(mavenSession.getRepositorySession(), rangeRequest);
307        return rangeResult.getVersions().stream()
308                .filter(version -> !ArtifactUtils.isSnapshot(version.toString()))
309                .collect(Collectors.toList());
310    }
311
312    private MavenProject buildMavenProject(String version) throws ProjectBuildingException {
313        MavenProject currentProject = mavenSession.getCurrentProject();
314        ProjectBuildingRequest buildRequest = new DefaultProjectBuildingRequest();
315        buildRequest.setLocalRepository(mavenSession.getLocalRepository());
316        buildRequest.setRemoteRepositories(mavenSession.getCurrentProject().getRemoteArtifactRepositories());
317        buildRequest.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL);
318        buildRequest.setProcessPlugins(false);
319        buildRequest.setRepositoryMerging(ProjectBuildingRequest.RepositoryMerging.REQUEST_DOMINANT);
320        buildRequest.setSystemProperties(mavenSession.getSystemProperties());
321        buildRequest.setUserProperties(mavenSession.getUserProperties());
322        buildRequest.setRepositorySession(mavenSession.getRepositorySession());
323        return projectBuilder
324                .build(
325                        RepositoryUtils.toArtifact(new DefaultArtifact(currentProject.getGroupId() + ":"
326                                + currentProject.getArtifactId() + ":pom:" + version)),
327                        buildRequest)
328                .getProject();
329    }
330}