1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.cling.executor.embedded;
20
21 import java.io.Closeable;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.PrintStream;
25 import java.lang.reflect.Constructor;
26 import java.lang.reflect.Method;
27 import java.net.MalformedURLException;
28 import java.net.URL;
29 import java.net.URLClassLoader;
30 import java.nio.file.Files;
31 import java.nio.file.Path;
32 import java.util.ArrayList;
33 import java.util.Collections;
34 import java.util.List;
35 import java.util.Properties;
36 import java.util.concurrent.ConcurrentHashMap;
37 import java.util.function.Function;
38 import java.util.stream.Stream;
39
40 import org.apache.maven.api.cli.Executor;
41 import org.apache.maven.api.cli.ExecutorException;
42 import org.apache.maven.api.cli.ExecutorRequest;
43
44 import static java.util.Objects.requireNonNull;
45
46
47
48
49
50
51
52 public class EmbeddedMavenExecutor implements Executor {
53 protected static final class Context {
54 private final Properties properties;
55 private final URLClassLoader bootClassLoader;
56 private final String version;
57 private final Object classWorld;
58 private final ClassLoader tccl;
59 private final Function<ExecutorRequest, Integer> exec;
60
61 public Context(
62 Properties properties,
63 URLClassLoader bootClassLoader,
64 String version,
65 Object classWorld,
66 ClassLoader tccl,
67 Function<ExecutorRequest, Integer> exec) {
68 this.properties = properties;
69 this.bootClassLoader = bootClassLoader;
70 this.version = version;
71 this.classWorld = classWorld;
72 this.tccl = tccl;
73 this.exec = exec;
74 }
75 }
76
77 private final Properties originalProperties;
78 private final ClassLoader originalClassLoader;
79 private final ConcurrentHashMap<Path, Context> contexts;
80
81 public EmbeddedMavenExecutor() {
82 this.originalClassLoader = Thread.currentThread().getContextClassLoader();
83 this.contexts = new ConcurrentHashMap<>();
84 this.originalProperties = System.getProperties();
85 }
86
87 @Override
88 public int execute(ExecutorRequest executorRequest) throws ExecutorException {
89 requireNonNull(executorRequest);
90 validate(executorRequest);
91 Context context = mayCreate(executorRequest);
92
93 System.setProperties(context.properties);
94 Thread.currentThread().setContextClassLoader(context.tccl);
95 try {
96 return context.exec.apply(executorRequest);
97 } catch (Exception e) {
98 throw new ExecutorException("Failed to execute", e);
99 } finally {
100 Thread.currentThread().setContextClassLoader(originalClassLoader);
101 System.setProperties(originalProperties);
102 }
103 }
104
105 @Override
106 public String mavenVersion(ExecutorRequest executorRequest) throws ExecutorException {
107 requireNonNull(executorRequest);
108 validate(executorRequest);
109 return mayCreate(executorRequest).version;
110 }
111
112 protected Context mayCreate(ExecutorRequest executorRequest) {
113 Path installation = executorRequest.installationDirectory();
114 if (!Files.isDirectory(installation)) {
115 throw new IllegalArgumentException("Installation directory must point to existing directory");
116 }
117 return contexts.computeIfAbsent(installation, k -> {
118 Path mavenHome = installation.toAbsolutePath().normalize();
119 Path boot = mavenHome.resolve("boot");
120 Path m2conf = mavenHome.resolve("bin/m2.conf");
121 if (!Files.isDirectory(boot) || !Files.isRegularFile(m2conf)) {
122 throw new IllegalArgumentException("Installation directory does not point to Maven installation");
123 }
124
125 Properties properties = new Properties();
126 properties.putAll(System.getProperties());
127 properties.put(
128 "user.dir",
129 executorRequest.cwd().toAbsolutePath().normalize().toString());
130 properties.put(
131 "maven.multiModuleProjectDirectory",
132 executorRequest.cwd().toAbsolutePath().normalize().toString());
133 properties.put(
134 "user.home",
135 executorRequest
136 .userHomeDirectory()
137 .toAbsolutePath()
138 .normalize()
139 .toString());
140 properties.put("maven.home", mavenHome.toString());
141 properties.put("maven.mainClass", "org.apache.maven.cling.MavenCling");
142 properties.put(
143 "library.jline.path", mavenHome.resolve("lib/jline-native").toString());
144
145 System.setProperties(properties);
146 URLClassLoader bootClassLoader = createMavenBootClassLoader(boot, Collections.emptyList());
147 Thread.currentThread().setContextClassLoader(bootClassLoader);
148 try {
149 Class<?> launcherClass = bootClassLoader.loadClass("org.codehaus.plexus.classworlds.launcher.Launcher");
150 Object launcher = launcherClass.getDeclaredConstructor().newInstance();
151 Method configure = launcherClass.getMethod("configure", InputStream.class);
152 try (InputStream inputStream = Files.newInputStream(m2conf)) {
153 configure.invoke(launcher, inputStream);
154 }
155 Object classWorld = launcherClass.getMethod("getWorld").invoke(launcher);
156 Class<?> cliClass =
157 (Class<?>) launcherClass.getMethod("getMainClass").invoke(launcher);
158 String version = getMavenVersion(cliClass);
159 Function<ExecutorRequest, Integer> exec;
160
161 if (version.startsWith("3.")) {
162
163 Constructor<?> newMavenCli = cliClass.getConstructor(classWorld.getClass());
164 Object mavenCli = newMavenCli.newInstance(classWorld);
165 Class<?>[] parameterTypes = {String[].class, String.class, PrintStream.class, PrintStream.class};
166 Method doMain = cliClass.getMethod("doMain", parameterTypes);
167 exec = r -> {
168 try {
169 return (int) doMain.invoke(mavenCli, new Object[] {
170 r.arguments().toArray(new String[0]), r.cwd().toString(), null, null
171 });
172 } catch (Exception e) {
173 throw new ExecutorException("Failed to execute", e);
174 }
175 };
176 } else {
177
178 Method mainMethod = cliClass.getMethod("main", String[].class, classWorld.getClass());
179 exec = r -> {
180 try {
181 return (int) mainMethod.invoke(null, r.arguments().toArray(new String[0]), classWorld);
182 } catch (Exception e) {
183 throw new ExecutorException("Failed to execute", e);
184 }
185 };
186 }
187
188 return new Context(properties, bootClassLoader, version, classWorld, cliClass.getClassLoader(), exec);
189 } catch (Exception e) {
190 throw new ExecutorException("Failed to create executor", e);
191 } finally {
192 Thread.currentThread().setContextClassLoader(originalClassLoader);
193 System.setProperties(originalProperties);
194 }
195 });
196 }
197
198 @Override
199 public void close() throws ExecutorException {
200 try {
201 ArrayList<Exception> exceptions = new ArrayList<>();
202 for (Context context : contexts.values()) {
203 try {
204 doClose(context);
205 } catch (Exception e) {
206 exceptions.add(e);
207 }
208 }
209 if (!exceptions.isEmpty()) {
210 ExecutorException e = new ExecutorException("Could not close cleanly");
211 exceptions.forEach(e::addSuppressed);
212 throw e;
213 }
214 } finally {
215 System.setProperties(originalProperties);
216 }
217 }
218
219 protected void doClose(Context context) throws Exception {
220 Thread.currentThread().setContextClassLoader(context.bootClassLoader);
221 try {
222 try {
223 ((Closeable) context.classWorld).close();
224 } finally {
225 context.bootClassLoader.close();
226 }
227 } finally {
228 Thread.currentThread().setContextClassLoader(originalClassLoader);
229 }
230 }
231
232 protected void validate(ExecutorRequest executorRequest) throws ExecutorException {}
233
234 protected URLClassLoader createMavenBootClassLoader(Path boot, List<URL> extraClasspath) {
235 ArrayList<URL> urls = new ArrayList<>(extraClasspath);
236 try (Stream<Path> stream = Files.list(boot)) {
237 stream.filter(Files::isRegularFile)
238 .filter(p -> p.toString().endsWith(".jar"))
239 .forEach(f -> {
240 try {
241 urls.add(f.toUri().toURL());
242 } catch (MalformedURLException e) {
243 throw new ExecutorException("Failed to build classpath: " + f, e);
244 }
245 });
246 } catch (IOException e) {
247 throw new ExecutorException("Failed to build classpath: " + e, e);
248 }
249 if (urls.isEmpty()) {
250 throw new IllegalArgumentException("Invalid Maven home directory; boot is empty");
251 }
252 return new URLClassLoader(
253 urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader().getParent());
254 }
255
256 protected String getMavenVersion(Class<?> clazz) throws IOException {
257 Properties props = new Properties();
258 try (InputStream is = clazz.getResourceAsStream("/META-INF/maven/org.apache.maven/maven-core/pom.properties")) {
259 if (is != null) {
260 props.load(is);
261 }
262 String version = props.getProperty("version");
263 if (version != null) {
264 return version;
265 }
266 return UNKNOWN_VERSION;
267 }
268 }
269 }