001/*
002 * Copyright 2017-2022 original authors
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.micronaut.maven.services;
017
018import io.micronaut.core.util.StringUtils;
019import org.apache.maven.execution.MavenSession;
020import org.apache.maven.model.Plugin;
021import org.apache.maven.model.PluginExecution;
022import org.apache.maven.plugin.BuildPluginManager;
023import org.apache.maven.plugin.MojoExecutionException;
024import org.apache.maven.project.MavenProject;
025import org.apache.maven.shared.invoker.DefaultInvocationRequest;
026import org.apache.maven.shared.invoker.InvocationResult;
027import org.apache.maven.shared.invoker.Invoker;
028import org.apache.maven.shared.invoker.MavenInvocationException;
029import org.codehaus.plexus.util.xml.Xpp3Dom;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032
033import javax.inject.Inject;
034import javax.inject.Singleton;
035import java.io.File;
036import java.util.Arrays;
037import java.util.Optional;
038import java.util.Properties;
039import java.util.concurrent.atomic.AtomicReference;
040
041import static io.micronaut.maven.testresources.TestResourcesConfiguration.TEST_RESOURCES_ENABLED_PROPERTY;
042import static org.twdata.maven.mojoexecutor.MojoExecutor.configuration;
043import static org.twdata.maven.mojoexecutor.MojoExecutor.executeMojo;
044import static org.twdata.maven.mojoexecutor.MojoExecutor.executionEnvironment;
045import static org.twdata.maven.mojoexecutor.MojoExecutor.goal;
046import static org.twdata.maven.mojoexecutor.MojoExecutor.plugin;
047
048/**
049 * Provides methods to execute goals on the current project.
050 *
051 * @author Álvaro Sánchez-Mariscal
052 * @since 1.1
053 */
054@Singleton
055public class ExecutorService {
056
057    private static final Logger LOG = LoggerFactory.getLogger(ExecutorService.class);
058
059    private final BuildPluginManager pluginManager;
060    private final MavenProject mavenProject;
061    private final MavenSession mavenSession;
062    private final Invoker invoker;
063
064    @SuppressWarnings("CdiInjectionPointsInspection")
065    @Inject
066    public ExecutorService(MavenProject mavenProject, MavenSession mavenSession, BuildPluginManager pluginManager,
067                           Invoker invoker) {
068        this.pluginManager = pluginManager;
069        this.mavenProject = mavenProject;
070        this.mavenSession = mavenSession;
071        this.invoker = invoker;
072    }
073
074    /**
075     * Executes the given goal from the given plugin coordinates.
076     *
077     * @param pluginKey The plugin coordinates in the format groupId:artifactId:version
078     * @param goal The goal to execute
079     * @throws MojoExecutionException If the goal execution fails
080     */
081    public void executeGoal(String pluginKey, String goal) throws MojoExecutionException {
082        executeGoal(mavenProject, pluginKey, goal, null);
083    }
084
085    public final void executeGoal(MavenProject project, String pluginKey, String goal) throws MojoExecutionException {
086        executeGoal(project, pluginKey, goal, null);
087    }
088
089    public final void executeGoal(MavenProject project, String pluginKey, String goal, Xpp3Dom overriddenConfiguration) throws MojoExecutionException {
090        MavenProject targetProject = project == null ? mavenProject : project;
091        final Plugin plugin = targetProject.getPlugin(pluginKey);
092        if (plugin != null) {
093            String goalName = goal;
094            var executionId = new AtomicReference<>(goalName);
095            if (goalName != null && goalName.indexOf('#') > -1) {
096                int pos = goalName.indexOf('#');
097                executionId.set(goal.substring(pos + 1));
098                goalName = goalName.substring(0, pos);
099            }
100            Optional<PluginExecution> execution = plugin
101                .getExecutions()
102                .stream()
103                .filter(e -> e.getId().equals(executionId.get()))
104                .findFirst();
105            Xpp3Dom configuration;
106            if (overriddenConfiguration != null) {
107                configuration = overriddenConfiguration;
108            } else if (execution.isPresent()) {
109                configuration = (Xpp3Dom) execution.get().getConfiguration();
110            } else if (plugin.getConfiguration() != null) {
111                configuration = (Xpp3Dom) plugin.getConfiguration();
112            } else {
113                configuration = configuration();
114            }
115            executeMojo(plugin, goal(goalName), configuration, executionEnvironment(targetProject, mavenSession, pluginManager));
116        } else {
117            throw new MojoExecutionException("Plugin not found: " + pluginKey);
118        }
119    }
120
121    /**
122     * Executes a goal using the given arguments.
123     *
124     * @param pluginGroup plugin group id
125     * @param pluginArtifact plugin artifact id
126     * @param pluginVersion plugin version
127     * @param goal goal to execute
128     * @param configuration configuration for the goal
129     * @throws MojoExecutionException if the goal execution fails
130     */
131    public void executeGoal(String pluginGroup, String pluginArtifact, String pluginVersion, String goal, Xpp3Dom configuration) throws MojoExecutionException {
132        final Plugin plugin = plugin(pluginGroup, pluginArtifact, pluginVersion);
133        executeMojo(plugin, goal(goal), configuration, executionEnvironment(mavenProject, mavenSession, pluginManager));
134    }
135
136    /**
137     * Executes a goal using the Maven shared invoker.
138     *
139     * @param pluginKey The plugin coordinates in the format groupId:artifactId
140     * @param goal The goal to execute
141     * @return The result of the invocation
142     * @throws MavenInvocationException If the goal execution fails
143     */
144    public InvocationResult invokeGoal(String pluginKey, String goal) throws MavenInvocationException {
145        return invokeGoals(pluginKey + ":" + goal);
146    }
147
148    /**
149     * Executes a goal using the Maven shared invoker.
150     *
151     * @param goals The goals to execute
152     * @return The result of the invocation
153     * @throws MavenInvocationException If the goal execution fails
154     */
155    public InvocationResult invokeGoals(String... goals) throws MavenInvocationException {
156        return invokeGoals(mavenProject, goals);
157    }
158
159    /**
160     * Executes a goal using the Maven shared invoker.
161     *
162     * @param project The Maven project
163     * @param goals The goals to execute
164     * @return The result of the invocation
165     * @throws MavenInvocationException If the goal execution fails
166     */
167
168    public InvocationResult invokeGoals(MavenProject project, String... goals) throws MavenInvocationException {
169        var request = new DefaultInvocationRequest();
170        request.setPomFile(resolveOriginalPom(project));
171        File settingsFile = mavenSession.getRequest().getUserSettingsFile();
172        if (settingsFile.exists()) {
173            request.setUserSettingsFile(settingsFile);
174        }
175        var properties = new Properties();
176        properties.put(TEST_RESOURCES_ENABLED_PROPERTY, StringUtils.FALSE);
177
178        request.setLocalRepositoryDirectory(new File(mavenSession.getLocalRepository().getBasedir()));
179        request.addArgs(Arrays.asList(goals));
180        request.setBatchMode(true);
181        request.setQuiet(true);
182        request.setAlsoMake(true);
183        request.setErrorHandler(LOG::error);
184        request.setOutputHandler(LOG::info);
185        request.setProperties(properties);
186        return invoker.execute(request);
187    }
188
189    /**
190     * Resolves the original pom.xml for a project. Plugins like the flatten-maven-plugin
191     * may modify the project's POM file to point to a processed POM (e.g., in the target
192     * directory). When the Maven invoker uses such a POM, {@code <module>} paths are resolved
193     * relative to the POM file's location, which causes module resolution failures in
194     * multi-module projects.
195     *
196     * @param project The Maven project
197     * @return The original pom.xml file, or the project's file if the original cannot be found
198     */
199    static File resolveOriginalPom(MavenProject project) {
200        File projectFile = project.getFile();
201        if (projectFile == null) {
202            return null;
203        }
204        if ("pom.xml".equals(projectFile.getName())) {
205            return projectFile;
206        }
207        // Only attempt to resolve an "original" pom.xml when the current project file
208        // looks like a processed POM produced during the build (for example, by the
209        // flatten-maven-plugin or maven-shade-plugin). This avoids overriding legitimate
210        // non-standard POM file names that may have been provided explicitly (e.g. via -f).
211        String buildDirectory = project.getBuild() != null ? project.getBuild().getDirectory() : null;
212        if (buildDirectory != null) {
213            File buildDir = new File(buildDirectory);
214            if (isInDirectory(projectFile, buildDir) || isKnownProcessedPom(projectFile)) {
215                // The build directory (typically {basedir}/target) is resolved from the original
216                // project directory during model building, so its parent should be the original
217                // project directory.
218                File projectDirectory = buildDir.getParentFile();
219                if (projectDirectory != null) {
220                    File originalPom = new File(projectDirectory, "pom.xml");
221                    if (originalPom.isFile()) {
222                        return originalPom;
223                    }
224                }
225            }
226        }
227        return projectFile;
228    }
229
230    /**
231     * Returns true if {@code file} is located in {@code directory} or one of its subdirectories.
232     */
233    private static boolean isInDirectory(File file, File directory) {
234        if (file == null || directory == null) {
235            return false;
236        }
237        File current = file.getParentFile();
238        while (current != null) {
239            if (current.equals(directory)) {
240                return true;
241            }
242            current = current.getParentFile();
243        }
244        return false;
245    }
246
247    /**
248     * Returns true if the given project file name matches a known processed/rewritten POM name.
249     */
250    private static boolean isKnownProcessedPom(File projectFile) {
251        if (projectFile == null) {
252            return false;
253        }
254        String name = projectFile.getName();
255        return "flattened-pom.xml".equals(name)
256            || ".flattened-pom.xml".equals(name)
257            || "dependency-reduced-pom.xml".equals(name);
258    }
259}