View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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   * Embedded executor implementation, that invokes Maven from installation directory within this same JVM but in isolated
48   * classloader. This class supports Maven 4.x and Maven 3.x as well.
49   * The class world with Maven is kept in memory as long as instance of this class is not closed. Subsequent execution
50   * requests over same installation home are cached.
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                     // 3.x
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                     // assume 4.x
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 }