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