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 org.apache.maven.execution.MavenExecutionRequest;
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 org.twdata.maven.mojoexecutor.MojoExecutor.configuration;
042import static org.twdata.maven.mojoexecutor.MojoExecutor.executeMojo;
043import static org.twdata.maven.mojoexecutor.MojoExecutor.executionEnvironment;
044import static org.twdata.maven.mojoexecutor.MojoExecutor.goal;
045import static org.twdata.maven.mojoexecutor.MojoExecutor.plugin;
046
047/**
048 * Provides methods to execute goals on the current project.
049 *
050 * @author Álvaro Sánchez-Mariscal
051 * @since 5.0.0
052 */
053@Singleton
054public class ExecutorService {
055
056    private static final String TEST_RESOURCES_ENABLED_PROPERTY = "micronaut.test.resources.enabled";
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,
067                           MavenSession mavenSession,
068                           BuildPluginManager pluginManager,
069                           Invoker invoker) {
070        this.pluginManager = pluginManager;
071        this.mavenProject = mavenProject;
072        this.mavenSession = mavenSession;
073        this.invoker = invoker;
074    }
075
076    /**
077     * Executes the given goal from the given plugin coordinates.
078     *
079     * @param pluginKey The plugin coordinates in the format groupId:artifactId:version
080     * @param goal The goal to execute
081     * @throws MojoExecutionException If the goal execution fails
082     */
083    public void executeGoal(String pluginKey, String goal) throws MojoExecutionException {
084        executeGoal(mavenProject, pluginKey, goal, null);
085    }
086
087    public final void executeGoal(MavenProject project, String pluginKey, String goal) throws MojoExecutionException {
088        executeGoal(project, pluginKey, goal, null);
089    }
090
091    public final void executeGoal(MavenProject project, String pluginKey, String goal, Xpp3Dom overriddenConfiguration) throws MojoExecutionException {
092        MavenProject targetProject = project == null ? mavenProject : project;
093        Plugin plugin = targetProject.getPlugin(pluginKey);
094        if (plugin != null) {
095            String goalName = goal;
096            AtomicReference<String> executionId = new AtomicReference<>(goalName);
097            if (goalName != null && goalName.indexOf('#') > -1) {
098                int pos = goalName.indexOf('#');
099                executionId.set(goal.substring(pos + 1));
100                goalName = goalName.substring(0, pos);
101            }
102            Optional<PluginExecution> execution = plugin.getExecutions()
103                .stream()
104                .filter(e -> e.getId().equals(executionId.get()))
105                .findFirst();
106            Xpp3Dom configuration;
107            if (overriddenConfiguration != null) {
108                configuration = overriddenConfiguration;
109            } else if (execution.isPresent()) {
110                configuration = (Xpp3Dom) execution.get().getConfiguration();
111            } else if (plugin.getConfiguration() != null) {
112                configuration = (Xpp3Dom) plugin.getConfiguration();
113            } else {
114                configuration = configuration();
115            }
116            executeMojo(plugin, goal(goalName), configuration, executionEnvironment(targetProject, mavenSession, pluginManager));
117        } else {
118            throw new MojoExecutionException("Plugin not found: " + pluginKey);
119        }
120    }
121
122    /**
123     * Executes a goal using the given arguments.
124     *
125     * @param pluginGroup plugin group id
126     * @param pluginArtifact plugin artifact id
127     * @param pluginVersion plugin version
128     * @param goal goal to execute
129     * @param configuration configuration for the goal
130     * @throws MojoExecutionException if the goal execution fails
131     */
132    public void executeGoal(String pluginGroup, String pluginArtifact, String pluginVersion, String goal, Xpp3Dom configuration) throws MojoExecutionException {
133        Plugin plugin = plugin(pluginGroup, pluginArtifact, pluginVersion);
134        executeMojo(plugin, goal(goal), configuration, executionEnvironment(mavenProject, mavenSession, pluginManager));
135    }
136
137    /**
138     * Executes a goal using the Maven shared invoker.
139     *
140     * @param pluginKey The plugin coordinates in the format groupId:artifactId
141     * @param goal The goal to execute
142     * @return The result of the invocation
143     * @throws MavenInvocationException If the goal execution fails
144     */
145    public InvocationResult invokeGoal(String pluginKey, String goal) throws MavenInvocationException {
146        return invokeGoals(pluginKey + ":" + goal);
147    }
148
149    /**
150     * Executes goals using the Maven shared invoker.
151     *
152     * @param goals The goals to execute
153     * @return The result of the invocation
154     * @throws MavenInvocationException If the goal execution fails
155     */
156    public InvocationResult invokeGoals(String... goals) throws MavenInvocationException {
157        return invokeGoals(mavenProject, goals);
158    }
159
160    /**
161     * Executes goals using the Maven shared invoker.
162     *
163     * @param project The Maven project
164     * @param goals The goals to execute
165     * @return The result of the invocation
166     * @throws MavenInvocationException If the goal execution fails
167     */
168    public InvocationResult invokeGoals(MavenProject project, String... goals) throws MavenInvocationException {
169        DefaultInvocationRequest request = new DefaultInvocationRequest();
170        request.setPomFile(resolveOriginalPom(project));
171        File settingsFile = mavenSession.getRequest().getUserSettingsFile();
172        if (settingsFile != null && settingsFile.exists()) {
173            request.setUserSettingsFile(settingsFile);
174        }
175        Properties properties = new Properties();
176        properties.put(TEST_RESOURCES_ENABLED_PROPERTY, "false");
177
178        int loggingLevel = mavenSession.getRequest().getLoggingLevel();
179        boolean quiet = loggingLevel >= MavenExecutionRequest.LOGGING_LEVEL_ERROR;
180
181        request.setLocalRepositoryDirectory(new File(mavenSession.getLocalRepository().getBasedir()));
182        request.addArgs(Arrays.asList(goals));
183        request.setBatchMode(true);
184        request.setQuiet(quiet);
185        request.setAlsoMake(true);
186        if (!quiet) {
187            request.setErrorHandler(LOG::error);
188            request.setOutputHandler(LOG::info);
189        }
190        request.setProperties(properties);
191        return invoker.execute(request);
192    }
193
194    /**
195     * Resolves the original pom.xml for a project. Plugins like the flatten-maven-plugin
196     * may modify the project's POM file to point to a processed POM (e.g., in the target
197     * directory). When the Maven invoker uses such a POM, {@code <module>} paths are resolved
198     * relative to the POM file's location, which causes module resolution failures in
199     * multi-module projects.
200     *
201     * @param project The Maven project
202     * @return The original pom.xml file, or the project's file if the original cannot be found
203     */
204    static File resolveOriginalPom(MavenProject project) {
205        File projectFile = project.getFile();
206        if (projectFile == null || "pom.xml".equals(projectFile.getName())) {
207            return projectFile;
208        }
209        String buildDirectory = project.getBuild() != null ? project.getBuild().getDirectory() : null;
210        if (buildDirectory != null) {
211            File buildDir = new File(buildDirectory);
212            if (isInDirectory(projectFile, buildDir) || isKnownProcessedPom(projectFile)) {
213                File projectDirectory = buildDir.getParentFile();
214                if (projectDirectory != null) {
215                    File originalPom = new File(projectDirectory, "pom.xml");
216                    if (originalPom.isFile()) {
217                        return originalPom;
218                    }
219                }
220            }
221        }
222        return projectFile;
223    }
224
225    private static boolean isInDirectory(File file, File directory) {
226        if (file == null || directory == null) {
227            return false;
228        }
229        File current = file.getParentFile();
230        while (current != null) {
231            if (current.equals(directory)) {
232                return true;
233            }
234            current = current.getParentFile();
235        }
236        return false;
237    }
238
239    private static boolean isKnownProcessedPom(File projectFile) {
240        if (projectFile == null) {
241            return false;
242        }
243        String name = projectFile.getName();
244        return "flattened-pom.xml".equals(name)
245            || ".flattened-pom.xml".equals(name)
246            || "dependency-reduced-pom.xml".equals(name);
247    }
248}