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.impl.standalone;
20  
21  import java.nio.file.Path;
22  import java.nio.file.Paths;
23  import java.time.Instant;
24  import java.util.Collection;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Objects;
31  import java.util.Optional;
32  import java.util.concurrent.ConcurrentHashMap;
33  import java.util.function.Consumer;
34  import java.util.stream.Collectors;
35  
36  import org.apache.maven.api.Artifact;
37  import org.apache.maven.api.Lifecycle;
38  import org.apache.maven.api.MonotonicClock;
39  import org.apache.maven.api.Packaging;
40  import org.apache.maven.api.ProducedArtifact;
41  import org.apache.maven.api.Project;
42  import org.apache.maven.api.RemoteRepository;
43  import org.apache.maven.api.Session;
44  import org.apache.maven.api.Type;
45  import org.apache.maven.api.Version;
46  import org.apache.maven.api.annotations.Nonnull;
47  import org.apache.maven.api.annotations.Nullable;
48  import org.apache.maven.api.di.Provides;
49  import org.apache.maven.api.di.SessionScoped;
50  import org.apache.maven.api.model.PluginContainer;
51  import org.apache.maven.api.model.Profile;
52  import org.apache.maven.api.services.ArtifactManager;
53  import org.apache.maven.api.services.LifecycleRegistry;
54  import org.apache.maven.api.services.Lookup;
55  import org.apache.maven.api.services.MavenException;
56  import org.apache.maven.api.services.PackagingRegistry;
57  import org.apache.maven.api.services.RepositoryFactory;
58  import org.apache.maven.api.services.SettingsBuilder;
59  import org.apache.maven.api.services.TypeRegistry;
60  import org.apache.maven.api.settings.Settings;
61  import org.apache.maven.api.spi.TypeProvider;
62  import org.apache.maven.api.toolchain.ToolchainModel;
63  import org.apache.maven.di.Injector;
64  import org.apache.maven.di.Key;
65  import org.apache.maven.di.impl.DIException;
66  import org.apache.maven.impl.AbstractSession;
67  import org.apache.maven.impl.InternalSession;
68  import org.apache.maven.impl.di.SessionScope;
69  import org.apache.maven.impl.resolver.scopes.Maven4ScopeManagerConfiguration;
70  import org.eclipse.aether.DefaultRepositorySystemSession;
71  import org.eclipse.aether.RepositorySystem;
72  import org.eclipse.aether.RepositorySystemSession;
73  import org.eclipse.aether.internal.impl.scope.ScopeManagerImpl;
74  import org.eclipse.aether.repository.LocalRepository;
75  import org.eclipse.aether.repository.LocalRepositoryManager;
76  
77  /**
78   * Provides functionality for running Maven API in a standalone mode.
79   * <p>
80   * This class serves as the main entry point for executing Maven operations outside
81   * of the standard Maven build environment. It provides methods for creating and
82   * managing Maven sessions in a simplified context, primarily for testing and
83   * specialized execution scenarios.
84   * </p>
85   *
86   * <p>Example usage:</p>
87   * <pre>
88   * Session session = ApiRunner.createSession();
89   * // Use session for Maven operations
90   * </pre>
91   *
92   * <p>
93   * The standalone mode provides a subset of Maven's functionality, with some
94   * features being unavailable or simplified. Operations not supported in
95   * standalone mode will throw {@link UnsupportedInStandaloneModeException}.
96   * </p>
97   *
98   * @since 4.0.0
99   */
100 public class ApiRunner {
101 
102     /**
103      * Creates a new Maven session with default configuration.
104      *
105      * @return a new {@link Session} instance
106      */
107     public static Session createSession() {
108         return createSession(null);
109     }
110 
111     /**
112      * Creates a new Maven session with custom injector configuration.
113      *
114      * @param injectorConsumer consumer function to customize the injector
115      * @return a new {@link Session} instance
116      */
117     public static Session createSession(Consumer<Injector> injectorConsumer) {
118         return createSession(injectorConsumer, null);
119     }
120 
121     /**
122      * Creates a new Maven session with custom injector configuration and local repository path.
123      *
124      * @param injectorConsumer consumer function to customize the injector
125      * @param localRepo path to the local repository
126      * @return a new {@link Session} instance
127      */
128     public static Session createSession(Consumer<Injector> injectorConsumer, Path localRepo) {
129         Injector injector = Injector.create();
130         injector.bindInstance(Injector.class, injector);
131         injector.bindImplicit(ApiRunner.class);
132         injector.bindImplicit(RepositorySystemSupplier.class);
133         injector.bindInstance(LocalRepoProvider.class, () -> localRepo);
134         injector.discover(ApiRunner.class.getClassLoader());
135         if (injectorConsumer != null) {
136             injectorConsumer.accept(injector);
137         }
138         Session session = injector.getInstance(Session.class);
139         SessionScope scope = new SessionScope();
140         scope.enter();
141         scope.seed(Session.class, session);
142         injector.bindScope(SessionScoped.class, scope);
143         return session;
144     }
145 
146     /**
147      * Interface for providing the local repository path.
148      */
149     interface LocalRepoProvider {
150         /**
151          * Gets the path to the local repository.
152          *
153          * @return the local repository path
154          */
155         Path getLocalRepo();
156     }
157 
158     /**
159      * Default implementation of the Maven session for standalone mode.
160      */
161     static class DefaultSession extends AbstractSession {
162 
163         private final Map<String, String> systemProperties;
164         private final Instant startTime = MonotonicClock.now();
165 
166         DefaultSession(RepositorySystemSession session, RepositorySystem repositorySystem, Lookup lookup) {
167             this(session, repositorySystem, Collections.emptyList(), null, lookup);
168         }
169 
170         protected DefaultSession(
171                 RepositorySystemSession session,
172                 RepositorySystem repositorySystem,
173                 List<RemoteRepository> repositories,
174                 List<org.eclipse.aether.repository.RemoteRepository> resolverRepositories,
175                 Lookup lookup) {
176             super(session, repositorySystem, repositories, resolverRepositories, lookup);
177             systemProperties = System.getenv().entrySet().stream()
178                     .collect(Collectors.toMap(e -> "env." + e.getKey(), Map.Entry::getValue));
179             System.getProperties().forEach((k, v) -> systemProperties.put(k.toString(), v.toString()));
180         }
181 
182         @Override
183         protected Session newSession(RepositorySystemSession session, List<RemoteRepository> repositories) {
184             return new DefaultSession(session, repositorySystem, repositories, null, lookup);
185         }
186 
187         @Override
188         public Settings getSettings() {
189             return Settings.newInstance();
190         }
191 
192         @Override
193         @Nonnull
194         public Collection<ToolchainModel> getToolchains() {
195             return List.of();
196         }
197 
198         @Override
199         public Map<String, String> getUserProperties() {
200             return Map.of();
201         }
202 
203         @Override
204         public Map<String, String> getSystemProperties() {
205             return systemProperties;
206         }
207 
208         @Override
209         public Map<String, String> getEffectiveProperties(Project project) {
210             HashMap<String, String> result = new HashMap<>(getSystemProperties());
211             if (project != null) {
212                 result.putAll(project.getModel().getProperties());
213             }
214             result.putAll(getUserProperties());
215             return result;
216         }
217 
218         @Override
219         public Version getMavenVersion() {
220             return null;
221         }
222 
223         @Override
224         public int getDegreeOfConcurrency() {
225             return 0;
226         }
227 
228         @Override
229         public Instant getStartTime() {
230             return startTime;
231         }
232 
233         @Override
234         public Path getTopDirectory() {
235             return null;
236         }
237 
238         @Override
239         public Path getRootDirectory() {
240             throw new IllegalStateException();
241         }
242 
243         @Override
244         public List<Project> getProjects() {
245             return List.of();
246         }
247 
248         @Override
249         public Map<String, Object> getPluginContext(Project project) {
250             throw new UnsupportedInStandaloneModeException();
251         }
252     }
253 
254     @Provides
255     @SuppressWarnings("unused")
256     static Lookup newLookup(Injector injector) {
257         return new Lookup() {
258             @Override
259             public <T> T lookup(Class<T> type) {
260                 try {
261                     return injector.getInstance(type);
262                 } catch (DIException e) {
263                     throw new MavenException("Unable to locate instance of type " + type, e);
264                 }
265             }
266 
267             @Override
268             public <T> T lookup(Class<T> type, String name) {
269                 try {
270                     return injector.getInstance(Key.of(type, name));
271                 } catch (DIException e) {
272                     throw new MavenException("Unable to locate instance of type " + type, e);
273                 }
274             }
275 
276             @Override
277             public <T> Optional<T> lookupOptional(Class<T> type) {
278                 try {
279                     return Optional.of(injector.getInstance(type));
280                 } catch (DIException e) {
281                     return Optional.empty();
282                 }
283             }
284 
285             @Override
286             public <T> Optional<T> lookupOptional(Class<T> type, String name) {
287                 try {
288                     return Optional.of(injector.getInstance(Key.of(type, name)));
289                 } catch (DIException e) {
290                     return Optional.empty();
291                 }
292             }
293 
294             @Override
295             public <T> List<T> lookupList(Class<T> type) {
296                 return injector.getInstance(new Key<List<T>>() {});
297             }
298 
299             @Override
300             public <T> Map<String, T> lookupMap(Class<T> type) {
301                 return injector.getInstance(new Key<Map<String, T>>() {});
302             }
303         };
304     }
305 
306     @Provides
307     @SuppressWarnings("unused")
308     static ArtifactManager newArtifactManager() {
309         return new ArtifactManager() {
310             private final Map<Artifact, Path> paths = new ConcurrentHashMap<>();
311 
312             @Override
313             public Optional<Path> getPath(Artifact artifact) {
314                 return Optional.ofNullable(paths.get(artifact));
315             }
316 
317             @Override
318             public void setPath(ProducedArtifact artifact, Path path) {
319                 paths.put(artifact, path);
320             }
321         };
322     }
323 
324     @Provides
325     @SuppressWarnings("unused")
326     static PackagingRegistry newPackagingRegistry(TypeRegistry typeRegistry) {
327         return id -> Optional.of(new DumbPackaging(id, typeRegistry.require(id), Map.of()));
328     }
329 
330     @Provides
331     @SuppressWarnings("unused")
332     static TypeRegistry newTypeRegistry(List<TypeProvider> providers) {
333         return new TypeRegistry() {
334             @Override
335             public Optional<Type> lookup(String id) {
336                 return providers.stream()
337                         .flatMap(p -> p.provides().stream())
338                         .filter(t -> Objects.equals(id, t.id()))
339                         .findAny();
340             }
341         };
342     }
343 
344     @Provides
345     @SuppressWarnings("unused")
346     static LifecycleRegistry newLifecycleRegistry() {
347         return new LifecycleRegistry() {
348 
349             @Override
350             public Iterator<Lifecycle> iterator() {
351                 return Collections.emptyIterator();
352             }
353 
354             @Override
355             public Optional<Lifecycle> lookup(String id) {
356                 return Optional.empty();
357             }
358 
359             @Override
360             public List<String> computePhases(Lifecycle lifecycle) {
361                 return List.of();
362             }
363         };
364     }
365 
366     @Provides
367     @SuppressWarnings("unused")
368     static Session newSession(RepositorySystem system, Lookup lookup, @Nullable LocalRepoProvider localRepoProvider) {
369         Map<String, String> properties = new HashMap<>();
370         // Env variables prefixed with "env."
371         System.getenv().forEach((k, v) -> properties.put("env." + k, v));
372         // Java System properties
373         System.getProperties().forEach((k, v) -> properties.put(k.toString(), v.toString()));
374 
375         // Do not allow user settings to interfere with our unit tests
376         // TODO: remove that when this go more public
377         properties.put("user.home", "target");
378 
379         Path userHome = Paths.get(properties.get("user.home"));
380         Path mavenUserHome = userHome.resolve(".m2");
381         Path mavenSystemHome = properties.containsKey("maven.home")
382                 ? Paths.get(properties.get("maven.home"))
383                 : properties.containsKey("env.MAVEN_HOME") ? Paths.get(properties.get("env.MAVEN_HOME")) : null;
384 
385         DefaultRepositorySystemSession rsession = new DefaultRepositorySystemSession(h -> false);
386         rsession.setScopeManager(new ScopeManagerImpl(Maven4ScopeManagerConfiguration.INSTANCE));
387         rsession.setSystemProperties(properties);
388         rsession.setConfigProperties(properties);
389 
390         DefaultSession session = new DefaultSession(
391                 rsession,
392                 system,
393                 List.of(lookup.lookup(RepositoryFactory.class)
394                         .createRemote("central", "https://repo.maven.apache.org/maven2")),
395                 null,
396                 lookup);
397 
398         Settings settings = session.getService(SettingsBuilder.class)
399                 .build(
400                         session,
401                         mavenSystemHome != null ? mavenSystemHome.resolve("settings.xml") : null,
402                         mavenUserHome.resolve("settings.xml"))
403                 .getEffectiveSettings();
404 
405         // local repository
406         String localRepository = settings.getLocalRepository() != null
407                         && !settings.getLocalRepository().isEmpty()
408                 ? settings.getLocalRepository()
409                 : localRepoProvider != null && localRepoProvider.getLocalRepo() != null
410                         ? localRepoProvider.getLocalRepo().toString()
411                         : mavenUserHome.resolve("repository").toString();
412         LocalRepositoryManager llm = system.newLocalRepositoryManager(rsession, new LocalRepository(localRepository));
413         rsession.setLocalRepositoryManager(llm);
414         // active proxies
415         // TODO
416         // active profiles
417 
418         Profile profile = session.getService(SettingsBuilder.class)
419                 .convert(org.apache.maven.api.settings.Profile.newBuilder()
420                         .repositories(settings.getRepositories())
421                         .pluginRepositories(settings.getPluginRepositories())
422                         .build());
423         RepositoryFactory repositoryFactory = session.getService(RepositoryFactory.class);
424         List<RemoteRepository> repositories = profile.getRepositories().stream()
425                 .map(repositoryFactory::createRemote)
426                 .toList();
427         InternalSession s = (InternalSession) session.withRemoteRepositories(repositories);
428         InternalSession.associate(rsession, s);
429         return s;
430 
431         // List<RemoteRepository> repositories = repositoryFactory.createRemote();
432 
433         //        session.getService(SettingsBuilder.class).convert()
434 
435         //        settings.getDelegate().getRepositories().stream()
436         //                        .map(r -> SettingsUtilsV4.)
437         //        defaultSession.getService(RepositoryFactory.class).createRemote()
438         //        return defaultSession;
439     }
440 
441     record DumbPackaging(String id, Type type, Map<String, PluginContainer> plugins) implements Packaging {}
442 }