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}