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 = resolveBuildPlugin(targetProject, 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    static Plugin resolveBuildPlugin(MavenProject targetProject, String pluginKey) {
123        Plugin plugin = targetProject.getPlugin(pluginKey);
124        if (plugin != null) {
125            return plugin;
126        }
127        if (targetProject.getBuild() == null || targetProject.getBuild().getPluginManagement() == null) {
128            return null;
129        }
130        return targetProject.getBuild().getPluginManagement().getPlugins()
131            .stream()
132            .filter(managedPlugin -> matchesPluginKey(managedPlugin, pluginKey))
133            .findFirst()
134            .orElse(null);
135    }
136
137    private static boolean matchesPluginKey(Plugin plugin, String pluginKey) {
138        if (plugin == null) {
139            return false;
140        }
141        if (pluginKey.equals(plugin.getKey())) {
142            return true;
143        }
144        String artifactId = plugin.getArtifactId();
145        if (artifactId == null) {
146            return false;
147        }
148        String version = plugin.getVersion();
149        return version != null && pluginKey.equals(plugin.getKey() + ":" + version);
150    }
151
152    /**
153     * Executes a goal using the given arguments.
154     *
155     * @param pluginGroup plugin group id
156     * @param pluginArtifact plugin artifact id
157     * @param pluginVersion plugin version
158     * @param goal goal to execute
159     * @param configuration configuration for the goal
160     * @throws MojoExecutionException if the goal execution fails
161     */
162    public void executeGoal(String pluginGroup, String pluginArtifact, String pluginVersion, String goal, Xpp3Dom configuration) throws MojoExecutionException {
163        Plugin plugin = plugin(pluginGroup, pluginArtifact, pluginVersion);
164        executeMojo(plugin, goal(goal), configuration, executionEnvironment(mavenProject, mavenSession, pluginManager));
165    }
166
167    /**
168     * Executes a goal using the Maven shared invoker.
169     *
170     * @param pluginKey The plugin coordinates in the format groupId:artifactId
171     * @param goal The goal to execute
172     * @return The result of the invocation
173     * @throws MavenInvocationException If the goal execution fails
174     */
175    public InvocationResult invokeGoal(String pluginKey, String goal) throws MavenInvocationException {
176        return invokeGoals(pluginKey + ":" + goal);
177    }
178
179    /**
180     * Executes goals using the Maven shared invoker.
181     *
182     * @param goals The goals to execute
183     * @return The result of the invocation
184     * @throws MavenInvocationException If the goal execution fails
185     */
186    public InvocationResult invokeGoals(String... goals) throws MavenInvocationException {
187        return invokeGoals(mavenProject, goals);
188    }
189
190    /**
191     * Executes goals using the Maven shared invoker.
192     *
193     * @param project The Maven project
194     * @param goals The goals to execute
195     * @return The result of the invocation
196     * @throws MavenInvocationException If the goal execution fails
197     */
198    public InvocationResult invokeGoals(MavenProject project, String... goals) throws MavenInvocationException {
199        DefaultInvocationRequest request = new DefaultInvocationRequest();
200        request.setPomFile(resolveOriginalPom(project));
201        File settingsFile = mavenSession.getRequest().getUserSettingsFile();
202        if (settingsFile != null && settingsFile.exists()) {
203            request.setUserSettingsFile(settingsFile);
204        }
205        Properties properties = new Properties();
206        properties.put(TEST_RESOURCES_ENABLED_PROPERTY, "false");
207
208        int loggingLevel = mavenSession.getRequest().getLoggingLevel();
209        boolean quiet = loggingLevel >= MavenExecutionRequest.LOGGING_LEVEL_ERROR;
210
211        request.setLocalRepositoryDirectory(new File(mavenSession.getLocalRepository().getBasedir()));
212        request.addArgs(Arrays.asList(goals));
213        request.setBatchMode(true);
214        request.setQuiet(quiet);
215        request.setAlsoMake(true);
216        if (!quiet) {
217            request.setErrorHandler(LOG::error);
218            request.setOutputHandler(LOG::info);
219        }
220        request.setProperties(properties);
221        return invoker.execute(request);
222    }
223
224    /**
225     * Resolves the original pom.xml for a project. Plugins like the flatten-maven-plugin
226     * may modify the project's POM file to point to a processed POM (e.g., in the target
227     * directory). When the Maven invoker uses such a POM, {@code <module>} paths are resolved
228     * relative to the POM file's location, which causes module resolution failures in
229     * multi-module projects.
230     *
231     * @param project The Maven project
232     * @return The original pom.xml file, or the project's file if the original cannot be found
233     */
234    static File resolveOriginalPom(MavenProject project) {
235        File projectFile = project.getFile();
236        if (projectFile == null || "pom.xml".equals(projectFile.getName())) {
237            return projectFile;
238        }
239        String buildDirectory = project.getBuild() != null ? project.getBuild().getDirectory() : null;
240        if (buildDirectory != null) {
241            File buildDir = new File(buildDirectory);
242            if (isInDirectory(projectFile, buildDir) || isKnownProcessedPom(projectFile)) {
243                File projectDirectory = buildDir.getParentFile();
244                if (projectDirectory != null) {
245                    File originalPom = new File(projectDirectory, "pom.xml");
246                    if (originalPom.isFile()) {
247                        return originalPom;
248                    }
249                }
250            }
251        }
252        return projectFile;
253    }
254
255    private static boolean isInDirectory(File file, File directory) {
256        if (file == null || directory == null) {
257            return false;
258        }
259        File current = file.getParentFile();
260        while (current != null) {
261            if (current.equals(directory)) {
262                return true;
263            }
264            current = current.getParentFile();
265        }
266        return false;
267    }
268
269    private static boolean isKnownProcessedPom(File projectFile) {
270        if (projectFile == null) {
271            return false;
272        }
273        String name = projectFile.getName();
274        return "flattened-pom.xml".equals(name)
275            || ".flattened-pom.xml".equals(name)
276            || "dependency-reduced-pom.xml".equals(name);
277    }
278}