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.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
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.Properties;
import java.util.Set;
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.Constants;
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.Named;
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.services.VersionParser;
import org.apache.maven.api.settings.Mirror;
import org.apache.maven.api.settings.Proxy;
import org.apache.maven.api.settings.Server;
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.di.impl.InjectorImpl;
import org.apache.maven.impl.AbstractSession;
import org.apache.maven.impl.InternalSession;
import org.apache.maven.impl.di.SessionScope;
import org.apache.maven.impl.model.DefaultInterpolator;
import org.apache.maven.impl.resolver.MavenSessionBuilderSupplier;
import org.apache.maven.impl.resolver.scopes.Maven4ScopeManagerConfiguration;
import org.codehaus.plexus.components.secdispatcher.Cipher;
import org.codehaus.plexus.components.secdispatcher.Dispatcher;
import org.codehaus.plexus.components.secdispatcher.MasterSource;
import org.codehaus.plexus.components.secdispatcher.internal.cipher.AESGCMNoPadding;
import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.LegacyDispatcher;
import org.codehaus.plexus.components.secdispatcher.internal.dispatchers.MasterDispatcher;
import org.codehaus.plexus.components.secdispatcher.internal.sources.EnvMasterSource;
import org.codehaus.plexus.components.secdispatcher.internal.sources.GpgAgentMasterSource;
import org.codehaus.plexus.components.secdispatcher.internal.sources.PinEntryMasterSource;
import org.codehaus.plexus.components.secdispatcher.internal.sources.SystemPropertyMasterSource;
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;
import org.eclipse.aether.util.repository.AuthenticationBuilder;
import org.eclipse.aether.util.repository.DefaultAuthenticationSelector;
import org.eclipse.aether.util.repository.DefaultMirrorSelector;
import org.eclipse.aether.util.repository.DefaultProxySelector;
/**
* 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, suitable for tools, IDE integrations,
* and specialized execution scenarios.
* </p>
*
* <p>The standalone session reads and applies the user's {@code settings.xml} including:</p>
* <ul>
* <li>Local repository location</li>
* <li>Server authentication (username, password, private key)</li>
* <li>Proxy configuration</li>
* <li>Mirror configuration</li>
* <li>Repository definitions from active profiles</li>
* <li>Offline mode</li>
* </ul>
*
* <p>It also loads {@code maven-system.properties} from the Maven configuration directory
* ({@code ${maven.home}/conf} or as configured via {@code maven.installation.conf}/{@code maven.conf})
* and from the user's Maven home ({@code ~/.m2/}), merging both into the session's system properties.
* User properties are loaded from {@code ~/.m2/maven-user.properties} and exposed via
* {@link Session#getUserProperties()}.</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 {
/**
* Controls how the standalone session handles settings encryption/decryption.
*
* <p>Maven settings ({@code settings.xml}) may contain encrypted server passwords.
* Decryption requires the {@code plexus-sec-dispatcher} library on the classpath.
* This enum lets callers control the behavior when dispatchers are (or are not) available.</p>
*/
public enum SecurityMode {
/** Do not attempt to configure security dispatchers. Encrypted passwords are passed through as-is. */
NONE,
/** Try to configure security dispatchers; silently skip if the required classes are not on the classpath. */
IF_AVAILABLE,
/**
* Try to configure security dispatchers; warn to {@code System.err} if the required classes
* are not on the classpath. This is the default.
*/
IF_AVAILABLE_WARN,
/** Security dispatchers are required; throw {@link MavenException} if they cannot be configured. */
REQUIRED
}
/**
* 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) {
return createSession(injectorConsumer, localRepo, SecurityMode.IF_AVAILABLE_WARN);
}
/**
* Creates a new Maven session with custom injector configuration, local repository path,
* and security mode.
*
* @param injectorConsumer consumer function to customize the injector
* @param localRepo path to the local repository
* @param securityMode controls how encrypted passwords in settings are handled
* @return a new {@link Session} instance
*/
public static Session createSession(
Consumer<Injector> injectorConsumer, Path localRepo, SecurityMode securityMode) {
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());
configureSecurityDispatchers(injector, securityMode != null ? securityMode : SecurityMode.IF_AVAILABLE_WARN);
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;
}
/**
* Attempts to bind security dispatcher classes for settings password decryption.
* The dispatchers (legacy and master) are loaded from the {@code plexus-sec-dispatcher}
* library. If the library is not on the classpath, the behavior depends on the
* {@link SecurityMode}.
*
* <p>If dispatchers are already bound (e.g., via {@code discover()} in tests or by a
* custom {@code injectorConsumer}), this method is a no-op to avoid duplicate bindings.</p>
*/
private static void configureSecurityDispatchers(Injector injector, SecurityMode mode) {
if (mode == SecurityMode.NONE) {
return;
}
try {
Class.forName("org.codehaus.plexus.components.secdispatcher.Dispatcher");
// Skip if dispatchers are already bound (e.g., from discover() scanning test classes)
if (!hasExistingDispatchers(injector)) {
injector.bindImplicit(SecDispatcherBindings.class);
}
} catch (ClassNotFoundException | NoClassDefFoundError e) {
switch (mode) {
case REQUIRED:
throw new MavenException(
"Security dispatchers required but plexus-sec-dispatcher is not on the classpath", e);
case IF_AVAILABLE_WARN:
System.err.println("WARNING: plexus-sec-dispatcher not available on classpath; "
+ "encrypted passwords in settings.xml will not be decrypted");
break;
default:
break;
}
}
}
private static boolean hasExistingDispatchers(Injector injector) {
if (injector instanceof InjectorImpl impl) {
Set<?> bindings = impl.getAllBindings(Dispatcher.class);
return bindings != null && !bindings.isEmpty();
}
return false;
}
/**
* 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();
private Settings settings;
private Version mavenVersion;
private Map<String, String> userProperties = Map.of();
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) {
DefaultSession newSession = new DefaultSession(session, repositorySystem, repositories, null, lookup);
newSession.settings = this.settings;
newSession.mavenVersion = this.mavenVersion;
newSession.userProperties = this.userProperties;
return newSession;
}
void setSettings(Settings settings) {
this.settings = settings;
}
void setMavenVersion(Version mavenVersion) {
this.mavenVersion = mavenVersion;
}
void setUserProperties(Map<String, String> userProperties) {
this.userProperties = userProperties != null ? userProperties : Map.of();
}
@Override
@Nonnull
public Settings getSettings() {
return settings != null ? settings : Settings.newInstance();
}
@Override
@Nonnull
public Collection<ToolchainModel> getToolchains() {
return List.of();
}
@Override
public Map<String, String> getUserProperties() {
return userProperties;
}
@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 mavenVersion;
}
@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()));
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;
// Load maven-system.properties: installation-level first (sets paths like maven.user.conf),
// then user-level (~/.m2/) for custom overrides. The installation-level file is a bootstrapper
// that in the CLI includes user and project-level files via ${includes}; since we don't support
// that directive, we load the user-level file explicitly.
Path mavenConf = resolveMavenConf(properties);
if (mavenConf != null) {
Map<String, String> systemFileProps =
loadMavenProperties(mavenConf.resolve("maven-system.properties"), properties);
properties.putAll(systemFileProps);
}
properties.putAll(loadMavenProperties(mavenUserHome.resolve("maven-system.properties"), properties));
// Load maven-user.properties from the user-level location (~/.m2/) only.
// The installation-level maven-user.properties (in ${maven.conf}/) contains Maven-internal
// configuration (cache config, conflict resolver) that is not appropriate for standalone use;
// it is a CLI bootstrapper that includes user and project-level files via ${includes}.
Map<String, String> userProperties = new HashMap<>();
userProperties.putAll(loadMavenProperties(mavenUserHome.resolve("maven-user.properties"), properties));
// Configure the resolver session with dependency resolution machinery
MavenSessionBuilderSupplier sessionBuilderSupplier = new MavenSessionBuilderSupplier(system, false);
DefaultRepositorySystemSession rsession = new DefaultRepositorySystemSession(h -> false);
rsession.setScopeManager(new ScopeManagerImpl(Maven4ScopeManagerConfiguration.INSTANCE));
rsession.setDependencyTraverser(sessionBuilderSupplier.getDependencyTraverser());
rsession.setDependencyManager(sessionBuilderSupplier.getDependencyManager(true));
rsession.setDependencySelector(sessionBuilderSupplier.getDependencySelector());
rsession.setDependencyGraphTransformer(sessionBuilderSupplier.getDependencyGraphTransformer());
rsession.setArtifactTypeRegistry(sessionBuilderSupplier.getArtifactTypeRegistry());
rsession.setArtifactDescriptorPolicy(sessionBuilderSupplier.getArtifactDescriptorPolicy());
rsession.setSystemProperties(properties);
rsession.setUserProperties(userProperties);
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();
// Store the effective settings and user properties on the session
session.setSettings(settings);
session.setUserProperties(userProperties);
// Set the Maven version
session.setMavenVersion(detectMavenVersion(lookup));
// 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);
// Apply offline mode from settings
if (settings.isOffline()) {
rsession.setOffline(true);
}
// Apply proxy configuration from settings
DefaultProxySelector proxySelector = new DefaultProxySelector();
for (Proxy proxy : settings.getProxies()) {
if (proxy.isActive()) {
AuthenticationBuilder authBuilder = new AuthenticationBuilder();
authBuilder.addUsername(proxy.getUsername()).addPassword(proxy.getPassword());
proxySelector.add(
new org.eclipse.aether.repository.Proxy(
proxy.getProtocol(), proxy.getHost(), proxy.getPort(), authBuilder.build()),
proxy.getNonProxyHosts());
}
}
rsession.setProxySelector(proxySelector);
// Apply mirror configuration from settings
DefaultMirrorSelector mirrorSelector = new DefaultMirrorSelector();
for (Mirror mirror : settings.getMirrors()) {
mirrorSelector.add(
mirror.getId(),
mirror.getUrl(),
mirror.getLayout(),
false,
mirror.isBlocked(),
mirror.getMirrorOf(),
mirror.getMirrorOfLayouts());
}
rsession.setMirrorSelector(mirrorSelector);
// Apply server authentication from settings
DefaultAuthenticationSelector authSelector = new DefaultAuthenticationSelector();
for (Server server : settings.getServers()) {
AuthenticationBuilder authBuilder = new AuthenticationBuilder();
authBuilder.addUsername(server.getUsername()).addPassword(server.getPassword());
authBuilder.addPrivateKey(server.getPrivateKey(), server.getPassphrase());
authSelector.add(server.getId(), authBuilder.build());
}
rsession.setAuthenticationSelector(authSelector);
// Build repositories from active profiles in settings
SettingsBuilder settingsBuilder = session.getService(SettingsBuilder.class);
Profile profile = settingsBuilder.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;
}
/**
* Resolves the Maven configuration directory following the same lookup order as the Maven CLI:
* {@code maven.installation.conf} ��� {@code maven.conf} ��� {@code ${maven.home}/conf} ��� {@code ${MAVEN_HOME}/conf}.
*
* @param properties the system properties (env + Java system properties)
* @return the maven conf directory path, or {@code null} if it cannot be determined
*/
private static Path resolveMavenConf(Map<String, String> properties) {
String installConf = properties.get(Constants.MAVEN_INSTALLATION_CONF);
if (installConf != null) {
return Paths.get(installConf);
}
String mavenConf = properties.get("maven.conf");
if (mavenConf != null) {
return Paths.get(mavenConf);
}
String mavenHome = properties.get(Constants.MAVEN_HOME);
if (mavenHome != null) {
return Paths.get(mavenHome).resolve("conf");
}
String envMavenHome = properties.get("env.MAVEN_HOME");
if (envMavenHome != null) {
return Paths.get(envMavenHome).resolve("conf");
}
return null;
}
/**
* Loads properties from a file and interpolates {@code ${...}} references against the given
* fallback properties. If the file does not exist or cannot be read, an empty map is returned.
*
* @param path the properties file to load
* @param fallback fallback values for interpolation (typically system properties)
* @return the loaded and interpolated properties
*/
private static Map<String, String> loadMavenProperties(Path path, Map<String, String> fallback) {
if (path == null || !Files.exists(path)) {
return new HashMap<>();
}
Properties fileProps = new Properties();
try (InputStream is = Files.newInputStream(path)) {
fileProps.load(is);
} catch (IOException e) {
// Silently ignore: properties files are optional configuration and the standalone
// API has no logger. Failing to read a file (permissions, concurrent deletion)
// should not prevent session creation ��� callers get the same behavior as if the
// file did not exist.
return new HashMap<>();
}
Map<String, String> result = new HashMap<>();
fileProps.forEach((k, v) -> result.put(k.toString(), v.toString()));
// Interpolate ${...} references against the loaded properties + fallback
for (Map.Entry<String, String> entry : result.entrySet()) {
String value = entry.getValue();
if (value != null && value.contains("${")) {
entry.setValue(
DefaultInterpolator.substVars(value, entry.getKey(), null, result, fallback::get, null, false));
}
}
return result;
}
/**
* Detects the Maven version by reading the pom.properties resource from the classpath.
* Falls back to reading from maven-impl's own pom.properties if maven-core is not available.
* If no version can be determined, returns {@code 0.0.0} as a sentinel value so that
* {@link Session#getMavenVersion()} is never null.
*
* @param lookup the lookup service
* @return the detected Maven version, never {@code null}
*/
@Nonnull
private static Version detectMavenVersion(Lookup lookup) {
String version = loadVersionFromProperties("META-INF/maven/org.apache.maven/maven-core/pom.properties");
if (version == null) {
version = loadVersionFromProperties("META-INF/maven/org.apache.maven/maven-impl/pom.properties");
}
if (version == null) {
version = "0.0.0";
}
try {
return lookup.lookup(VersionParser.class).parseVersion(version);
} catch (Exception e) {
// Should not happen with "0.0.0", but be safe
return lookup.lookup(VersionParser.class).parseVersion("0.0.0");
}
}
private static String loadVersionFromProperties(String resource) {
try (InputStream is = ApiRunner.class.getResourceAsStream("/" + resource)) {
if (is != null) {
Properties props = new Properties();
props.load(is);
String version = props.getProperty("version", "").trim();
if (!version.isEmpty() && !version.startsWith("${")) {
return version;
}
}
} catch (IOException e) {
// ignore
}
return null;
}
record DumbPackaging(String id, Type type, Map<String, PluginContainer> plugins) implements Packaging {}
/**
* Provides the security dispatcher bindings needed for decrypting encrypted passwords
* in settings.xml. This class is only loaded when {@code plexus-sec-dispatcher} is confirmed
* to be on the classpath (see {@link #configureSecurityDispatchers}).
*
* <p>Supports both legacy ({@code {...}}) and Maven 4 master-key-based encrypted passwords.</p>
*/
@SuppressWarnings("unused")
static class SecDispatcherBindings {
@Provides
@Named(LegacyDispatcher.NAME)
static Dispatcher legacyDispatcher() {
return new LegacyDispatcher();
}
@Provides
@Named(MasterDispatcher.NAME)
static Dispatcher masterDispatcher(Map<String, Cipher> ciphers, Map<String, MasterSource> sources) {
return new MasterDispatcher(ciphers, sources);
}
@Provides
@Named(AESGCMNoPadding.CIPHER_ALG)
static Cipher aesCipher() {
return new AESGCMNoPadding();
}
@Provides
@Named(EnvMasterSource.NAME)
static MasterSource envSource() {
return new EnvMasterSource();
}
@Provides
@Named(GpgAgentMasterSource.NAME)
static MasterSource gpgAgentSource() {
return new GpgAgentMasterSource();
}
@Provides
@Named(PinEntryMasterSource.NAME)
static MasterSource pinEntrySource() {
return new PinEntryMasterSource();
}
@Provides
@Named(SystemPropertyMasterSource.NAME)
static MasterSource systemPropertySource() {
return new SystemPropertyMasterSource();
}
}
}