ApiRunner.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.impl.standalone;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.apache.maven.api.Artifact;
import org.apache.maven.api.Lifecycle;
import org.apache.maven.api.MonotonicClock;
import org.apache.maven.api.Packaging;
import org.apache.maven.api.ProducedArtifact;
import org.apache.maven.api.Project;
import org.apache.maven.api.RemoteRepository;
import org.apache.maven.api.Session;
import org.apache.maven.api.Type;
import org.apache.maven.api.Version;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.di.Provides;
import org.apache.maven.api.di.SessionScoped;
import org.apache.maven.api.model.PluginContainer;
import org.apache.maven.api.model.Profile;
import org.apache.maven.api.services.ArtifactManager;
import org.apache.maven.api.services.LifecycleRegistry;
import org.apache.maven.api.services.Lookup;
import org.apache.maven.api.services.MavenException;
import org.apache.maven.api.services.PackagingRegistry;
import org.apache.maven.api.services.RepositoryFactory;
import org.apache.maven.api.services.SettingsBuilder;
import org.apache.maven.api.services.TypeRegistry;
import org.apache.maven.api.settings.Settings;
import org.apache.maven.api.spi.TypeProvider;
import org.apache.maven.api.toolchain.ToolchainModel;
import org.apache.maven.di.Injector;
import org.apache.maven.di.Key;
import org.apache.maven.di.impl.DIException;
import org.apache.maven.impl.AbstractSession;
import org.apache.maven.impl.InternalSession;
import org.apache.maven.impl.di.SessionScope;
import org.apache.maven.impl.resolver.scopes.Maven4ScopeManagerConfiguration;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.internal.impl.scope.ScopeManagerImpl;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.LocalRepositoryManager;
/**
* Provides functionality for running Maven API in a standalone mode.
* <p>
* This class serves as the main entry point for executing Maven operations outside
* of the standard Maven build environment. It provides methods for creating and
* managing Maven sessions in a simplified context, primarily for testing and
* specialized execution scenarios.
* </p>
*
* <p>Example usage:</p>
* <pre>
* Session session = ApiRunner.createSession();
* // Use session for Maven operations
* </pre>
*
* <p>
* The standalone mode provides a subset of Maven's functionality, with some
* features being unavailable or simplified. Operations not supported in
* standalone mode will throw {@link UnsupportedInStandaloneModeException}.
* </p>
*
* @since 4.0.0
*/
public class ApiRunner {
/**
* Creates a new Maven session with default configuration.
*
* @return a new {@link Session} instance
*/
public static Session createSession() {
return createSession(null);
}
/**
* Creates a new Maven session with custom injector configuration.
*
* @param injectorConsumer consumer function to customize the injector
* @return a new {@link Session} instance
*/
public static Session createSession(Consumer<Injector> injectorConsumer) {
return createSession(injectorConsumer, null);
}
/**
* Creates a new Maven session with custom injector configuration and local repository path.
*
* @param injectorConsumer consumer function to customize the injector
* @param localRepo path to the local repository
* @return a new {@link Session} instance
*/
public static Session createSession(Consumer<Injector> injectorConsumer, Path localRepo) {
Injector injector = Injector.create();
injector.bindInstance(Injector.class, injector);
injector.bindImplicit(ApiRunner.class);
injector.bindImplicit(RepositorySystemSupplier.class);
injector.bindInstance(LocalRepoProvider.class, () -> localRepo);
injector.discover(ApiRunner.class.getClassLoader());
if (injectorConsumer != null) {
injectorConsumer.accept(injector);
}
Session session = injector.getInstance(Session.class);
SessionScope scope = new SessionScope();
scope.enter();
scope.seed(Session.class, session);
injector.bindScope(SessionScoped.class, scope);
return session;
}
/**
* Interface for providing the local repository path.
*/
interface LocalRepoProvider {
/**
* Gets the path to the local repository.
*
* @return the local repository path
*/
Path getLocalRepo();
}
/**
* Default implementation of the Maven session for standalone mode.
*/
static class DefaultSession extends AbstractSession {
private final Map<String, String> systemProperties;
private final Instant startTime = MonotonicClock.now();
DefaultSession(RepositorySystemSession session, RepositorySystem repositorySystem, Lookup lookup) {
this(session, repositorySystem, Collections.emptyList(), null, lookup);
}
protected DefaultSession(
RepositorySystemSession session,
RepositorySystem repositorySystem,
List<RemoteRepository> repositories,
List<org.eclipse.aether.repository.RemoteRepository> resolverRepositories,
Lookup lookup) {
super(session, repositorySystem, repositories, resolverRepositories, lookup);
systemProperties = System.getenv().entrySet().stream()
.collect(Collectors.toMap(e -> "env." + e.getKey(), Map.Entry::getValue));
System.getProperties().forEach((k, v) -> systemProperties.put(k.toString(), v.toString()));
}
@Override
protected Session newSession(RepositorySystemSession session, List<RemoteRepository> repositories) {
return new DefaultSession(session, repositorySystem, repositories, null, lookup);
}
@Override
public Settings getSettings() {
return Settings.newInstance();
}
@Override
@Nonnull
public Collection<ToolchainModel> getToolchains() {
return List.of();
}
@Override
public Map<String, String> getUserProperties() {
return Map.of();
}
@Override
public Map<String, String> getSystemProperties() {
return systemProperties;
}
@Override
public Map<String, String> getEffectiveProperties(Project project) {
HashMap<String, String> result = new HashMap<>(getSystemProperties());
if (project != null) {
result.putAll(project.getModel().getProperties());
}
result.putAll(getUserProperties());
return result;
}
@Override
public Version getMavenVersion() {
return null;
}
@Override
public int getDegreeOfConcurrency() {
return 0;
}
@Override
public Instant getStartTime() {
return startTime;
}
@Override
public Path getTopDirectory() {
return null;
}
@Override
public Path getRootDirectory() {
throw new IllegalStateException();
}
@Override
public List<Project> getProjects() {
return List.of();
}
@Override
public Map<String, Object> getPluginContext(Project project) {
throw new UnsupportedInStandaloneModeException();
}
}
@Provides
@SuppressWarnings("unused")
static Lookup newLookup(Injector injector) {
return new Lookup() {
@Override
public <T> T lookup(Class<T> type) {
try {
return injector.getInstance(type);
} catch (DIException e) {
throw new MavenException("Unable to locate instance of type " + type, e);
}
}
@Override
public <T> T lookup(Class<T> type, String name) {
try {
return injector.getInstance(Key.of(type, name));
} catch (DIException e) {
throw new MavenException("Unable to locate instance of type " + type, e);
}
}
@Override
public <T> Optional<T> lookupOptional(Class<T> type) {
try {
return Optional.of(injector.getInstance(type));
} catch (DIException e) {
return Optional.empty();
}
}
@Override
public <T> Optional<T> lookupOptional(Class<T> type, String name) {
try {
return Optional.of(injector.getInstance(Key.of(type, name)));
} catch (DIException e) {
return Optional.empty();
}
}
@Override
public <T> List<T> lookupList(Class<T> type) {
return injector.getInstance(new Key<List<T>>() {});
}
@Override
public <T> Map<String, T> lookupMap(Class<T> type) {
return injector.getInstance(new Key<Map<String, T>>() {});
}
};
}
@Provides
@SuppressWarnings("unused")
static ArtifactManager newArtifactManager() {
return new ArtifactManager() {
private final Map<Artifact, Path> paths = new ConcurrentHashMap<>();
@Override
public Optional<Path> getPath(Artifact artifact) {
return Optional.ofNullable(paths.get(artifact));
}
@Override
public void setPath(ProducedArtifact artifact, Path path) {
paths.put(artifact, path);
}
};
}
@Provides
@SuppressWarnings("unused")
static PackagingRegistry newPackagingRegistry(TypeRegistry typeRegistry) {
return id -> Optional.of(new DumbPackaging(id, typeRegistry.require(id), Map.of()));
}
@Provides
@SuppressWarnings("unused")
static TypeRegistry newTypeRegistry(List<TypeProvider> providers) {
return new TypeRegistry() {
@Override
public Optional<Type> lookup(String id) {
return providers.stream()
.flatMap(p -> p.provides().stream())
.filter(t -> Objects.equals(id, t.id()))
.findAny();
}
};
}
@Provides
@SuppressWarnings("unused")
static LifecycleRegistry newLifecycleRegistry() {
return new LifecycleRegistry() {
@Override
public Iterator<Lifecycle> iterator() {
return Collections.emptyIterator();
}
@Override
public Optional<Lifecycle> lookup(String id) {
return Optional.empty();
}
@Override
public List<String> computePhases(Lifecycle lifecycle) {
return List.of();
}
};
}
@Provides
@SuppressWarnings("unused")
static Session newSession(RepositorySystem system, Lookup lookup, @Nullable LocalRepoProvider localRepoProvider) {
Map<String, String> properties = new HashMap<>();
// Env variables prefixed with "env."
System.getenv().forEach((k, v) -> properties.put("env." + k, v));
// Java System properties
System.getProperties().forEach((k, v) -> properties.put(k.toString(), v.toString()));
// Do not allow user settings to interfere with our unit tests
// TODO: remove that when this go more public
properties.put("user.home", "target");
Path userHome = Paths.get(properties.get("user.home"));
Path mavenUserHome = userHome.resolve(".m2");
Path mavenSystemHome = properties.containsKey("maven.home")
? Paths.get(properties.get("maven.home"))
: properties.containsKey("env.MAVEN_HOME") ? Paths.get(properties.get("env.MAVEN_HOME")) : null;
DefaultRepositorySystemSession rsession = new DefaultRepositorySystemSession(h -> false);
rsession.setScopeManager(new ScopeManagerImpl(Maven4ScopeManagerConfiguration.INSTANCE));
rsession.setSystemProperties(properties);
rsession.setConfigProperties(properties);
DefaultSession session = new DefaultSession(
rsession,
system,
List.of(lookup.lookup(RepositoryFactory.class)
.createRemote("central", "https://repo.maven.apache.org/maven2")),
null,
lookup);
Settings settings = session.getService(SettingsBuilder.class)
.build(
session,
mavenSystemHome != null ? mavenSystemHome.resolve("settings.xml") : null,
mavenUserHome.resolve("settings.xml"))
.getEffectiveSettings();
// local repository
String localRepository = settings.getLocalRepository() != null
&& !settings.getLocalRepository().isEmpty()
? settings.getLocalRepository()
: localRepoProvider != null && localRepoProvider.getLocalRepo() != null
? localRepoProvider.getLocalRepo().toString()
: mavenUserHome.resolve("repository").toString();
LocalRepositoryManager llm = system.newLocalRepositoryManager(rsession, new LocalRepository(localRepository));
rsession.setLocalRepositoryManager(llm);
// active proxies
// TODO
// active profiles
Profile profile = session.getService(SettingsBuilder.class)
.convert(org.apache.maven.api.settings.Profile.newBuilder()
.repositories(settings.getRepositories())
.pluginRepositories(settings.getPluginRepositories())
.build());
RepositoryFactory repositoryFactory = session.getService(RepositoryFactory.class);
List<RemoteRepository> repositories = profile.getRepositories().stream()
.map(repositoryFactory::createRemote)
.toList();
InternalSession s = (InternalSession) session.withRemoteRepositories(repositories);
InternalSession.associate(rsession, s);
return s;
// List<RemoteRepository> repositories = repositoryFactory.createRemote();
// session.getService(SettingsBuilder.class).convert()
// settings.getDelegate().getRepositories().stream()
// .map(r -> SettingsUtilsV4.)
// defaultSession.getService(RepositoryFactory.class).createRemote()
// return defaultSession;
}
record DumbPackaging(String id, Type type, Map<String, PluginContainer> plugins) implements Packaging {}
}