GH-1272 Remove Maven Resource Resolver dependency

This commit effectively removes dependency on spring-cloud-deployer

Resolves #1272
This commit is contained in:
Oleg Zhurakousky
2025-04-22 17:23:32 +02:00
parent ee5d717887
commit 1db9ea34ce
8 changed files with 1489 additions and 8 deletions

View File

@@ -15,6 +15,10 @@
<properties>
<java.version>17</java.version>
<commons-io.version>2.16.1</commons-io.version>
<maven.version>3.9.6</maven.version>
<maven-resolver.version>1.9.18</maven-resolver.version>
<maven-wagon.version>3.5.3</maven-wagon.version>
</properties>
<dependencies>
@@ -34,11 +38,7 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-deployer-resource-maven</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -55,6 +55,66 @@
<version>2.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-model-builder</artifactId>
<version>${maven.version}</version>
<!-- @TODO boot3 remove when updated -->
<exclusions>
<exclusion>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-resolver-provider</artifactId>
<version>${maven.version}</version>
<!-- @TODO boot3 remove when updated -->
<exclusions>
<exclusion>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-connector-basic</artifactId>
<version>${maven-resolver.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-transport-file</artifactId>
<version>${maven-resolver.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-transport-http</artifactId>
<version>${maven-resolver.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-transport-wagon</artifactId>
<version>${maven-resolver.version}</version>
</dependency>
<dependency>
<groupId>org.apache.maven.resolver</groupId>
<artifactId>maven-resolver-impl</artifactId>
<version>${maven-resolver.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.wagon</groupId>
<artifactId>wagon-http</artifactId>
<version>3.5.3</version>
</dependency>
</dependencies>
<build>

View File

@@ -34,10 +34,10 @@ import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;
import org.springframework.boot.loader.archive.JarFileArchive;
import org.springframework.cloud.deployer.resource.maven.MavenProperties;
import org.springframework.cloud.deployer.resource.maven.MavenResourceLoader;
import org.springframework.cloud.function.context.FunctionProperties;
import org.springframework.cloud.function.context.FunctionRegistry;
import org.springframework.cloud.function.deployer.utils.MavenProperties;
import org.springframework.cloud.function.deployer.utils.MavenResourceLoader;
import org.springframework.context.ApplicationContext;
import org.springframework.context.SmartLifecycle;
import org.springframework.context.annotation.Bean;

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2019-2025 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.function.deployer.utils;
import org.eclipse.aether.AbstractRepositoryListener;
import org.eclipse.aether.RepositoryEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Corneil du Plessis
*/
public class LoggingRepositoryListener extends AbstractRepositoryListener {
private static final Logger logger = LoggerFactory.getLogger(LoggingRepositoryListener.class);
public void artifactDeployed(RepositoryEvent event) {
println("artifactDeployed", event.getArtifact() + " to " + event.getRepository());
}
public void artifactDeploying(RepositoryEvent event) {
println("artifactDeploying", event.getArtifact() + " to " + event.getRepository());
}
public void artifactDescriptorInvalid(RepositoryEvent event) {
println("artifactDescriptorInvalid", "for " + event.getArtifact() + ": " + event.getException().getMessage());
}
public void artifactDescriptorMissing(RepositoryEvent event) {
println("artifactDescriptorMissing", "for " + event.getArtifact());
}
public void artifactInstalled(RepositoryEvent event) {
println("artifactInstalled", event.getArtifact() + " to " + event.getFile());
}
public void artifactInstalling(RepositoryEvent event) {
println("artifactInstalling", event.getArtifact() + " to " + event.getFile());
}
public void artifactResolved(RepositoryEvent event) {
println("artifactResolved", event.getArtifact() + " from " + event.getRepository());
}
public void artifactDownloading(RepositoryEvent event) {
println("artifactDownloading", event.getArtifact() + " from " + event.getRepository());
}
public void artifactDownloaded(RepositoryEvent event) {
println("artifactDownloaded", event.getArtifact() + " from " + event.getRepository());
}
public void artifactResolving(RepositoryEvent event) {
println("artifactResolving", event.getArtifact().toString());
}
public void metadataDeployed(RepositoryEvent event) {
println("metadataDeployed", event.getMetadata() + " to " + event.getRepository());
}
public void metadataDeploying(RepositoryEvent event) {
println("metadataDeploying", event.getMetadata() + " to " + event.getRepository());
}
public void metadataInstalled(RepositoryEvent event) {
println("metadataInstalled", event.getMetadata() + " to " + event.getFile());
}
public void metadataInstalling(RepositoryEvent event) {
println("metadataInstalling", event.getMetadata() + " to " + event.getFile());
}
public void metadataInvalid(RepositoryEvent event) {
println("metadataInvalid", event.getMetadata().toString());
}
public void metadataResolved(RepositoryEvent event) {
println("metadataResolved", event.getMetadata() + " from " + event.getRepository());
}
public void metadataResolving(RepositoryEvent event) {
println("metadataResolving", event.getMetadata() + " from " + event.getRepository());
}
private void println(String event, String message) {
logger.info("Aether Repository - " + event + ": " + message);
}
}

View File

@@ -0,0 +1,431 @@
/*
* Copyright 2019-2025 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.function.deployer.utils;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.ConfigurationProperties;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.RepositorySystemSession;
import org.eclipse.aether.artifact.Artifact;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.repository.Authentication;
import org.eclipse.aether.repository.AuthenticationContext;
import org.eclipse.aether.repository.AuthenticationDigest;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.Proxy;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.repository.RepositoryPolicy;
import org.eclipse.aether.resolution.ArtifactRequest;
import org.eclipse.aether.resolution.ArtifactResolutionException;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.resolution.VersionRangeRequest;
import org.eclipse.aether.resolution.VersionRangeResolutionException;
import org.eclipse.aether.resolution.VersionRangeResult;
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.transport.file.FileTransporterFactory;
import org.eclipse.aether.transport.http.HttpTransporterFactory;
import org.eclipse.aether.util.artifact.JavaScopes;
import org.eclipse.aether.util.repository.DefaultProxySelector;
import org.eclipse.aether.version.Version;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
/**
* Resolves a {@link MavenResource} to
* locate the artifact (uber jar) in a local Maven repository, downloading the latest update from a
* remote repository if necessary.
* <p>A set of default remote repos (Maven Central, Spring Snapshots, Spring Milestones) will be automatically added to
* the head of the list of remote repos. If the default repo is already explicitly configured (exact match on the repo url)
* then that particular default will be omitted. To skip the automatic default repos behavior altogether, set the
* {@link MavenProperties#isIncludeDefaultRemoteRepos()} property to {@code false}.
*
* @author David Turanski
* @author Mark Fisher
* @author Marius Bogoevici
* @author Ilayaperumal Gopinathan
* @author Donovan Muller
* @author Corneil du Plessis
* @author Chris Bono
*/
class MavenArtifactResolver {
private static final Logger logger = LoggerFactory.getLogger(MavenArtifactResolver.class);
private static final String DEFAULT_CONTENT_TYPE = "default";
private final RepositorySystem repositorySystem;
private final MavenProperties properties;
private final List<RemoteRepository> remoteRepositories = new LinkedList<>();
private final Authentication proxyAuthentication;
/**
* Create an instance using the provided properties.
*
* @param properties the properties for the maven repositories, proxies, and authentication
*/
MavenArtifactResolver(MavenProperties properties) {
Assert.notNull(properties, "MavenProperties must not be null");
Assert.notNull(properties.getLocalRepository(), "Local repository path cannot be null");
this.properties = properties;
if (logger.isDebugEnabled()) {
logger.debug("Configured local repository: " + properties.getLocalRepository());
logger.debug("Configured remote repositories: " + configuredRemoteRepositoriesDescription());
}
if (isProxyEnabled() && proxyHasCredentials()) {
final String username = this.properties.getProxy().getAuth().getUsername();
final String password = this.properties.getProxy().getAuth().getPassword();
this.proxyAuthentication = newAuthentication(username, password);
}
else {
this.proxyAuthentication = null;
}
File localRepository = new File(this.properties.getLocalRepository());
if (!localRepository.exists()) {
boolean created = localRepository.mkdirs();
// May have been created by another thread after above check. Double check.
Assert.isTrue(created || localRepository.exists(),
"Unable to create directory for local repository: " + localRepository);
}
Map<String, String> defaultRepoUrlsToIds = defaultRemoteRepos();
for (Map.Entry<String, MavenProperties.RemoteRepository> entry : this.properties.getRemoteRepositories()
.entrySet()) {
MavenProperties.RemoteRepository remoteRepository = entry.getValue();
RemoteRepository.Builder remoteRepositoryBuilder = new RemoteRepository.Builder(
entry.getKey(), DEFAULT_CONTENT_TYPE, remoteRepository.getUrl());
// Update policies when set.
if (remoteRepository.getPolicy() != null) {
remoteRepositoryBuilder.setPolicy(new RepositoryPolicy(remoteRepository.getPolicy().isEnabled(),
remoteRepository.getPolicy().getUpdatePolicy(),
remoteRepository.getPolicy().getChecksumPolicy()));
}
if (remoteRepository.getReleasePolicy() != null) {
remoteRepositoryBuilder
.setReleasePolicy(new RepositoryPolicy(remoteRepository.getReleasePolicy().isEnabled(),
remoteRepository.getReleasePolicy().getUpdatePolicy(),
remoteRepository.getReleasePolicy().getChecksumPolicy()));
}
if (remoteRepository.getSnapshotPolicy() != null) {
remoteRepositoryBuilder
.setSnapshotPolicy(new RepositoryPolicy(remoteRepository.getSnapshotPolicy().isEnabled(),
remoteRepository.getSnapshotPolicy().getUpdatePolicy(),
remoteRepository.getSnapshotPolicy().getChecksumPolicy()));
}
if (remoteRepositoryHasCredentials(remoteRepository)) {
final String username = remoteRepository.getAuth().getUsername();
final String password = remoteRepository.getAuth().getPassword();
remoteRepositoryBuilder.setAuthentication(newAuthentication(username, password));
}
// do not add default repo if explicitly configured
defaultRepoUrlsToIds.remove(remoteRepository.getUrl());
RemoteRepository repo = proxyRepoIfProxyEnabled(remoteRepositoryBuilder.build());
this.remoteRepositories.add(repo);
}
if (!defaultRepoUrlsToIds.isEmpty() && this.properties.isIncludeDefaultRemoteRepos()) {
List<RemoteRepository> defaultRepos = new ArrayList<>();
defaultRepoUrlsToIds.forEach((url, id) -> {
if (logger.isDebugEnabled()) {
logger.debug("Adding {} ({}) to remote repositories list", id, url);
}
RemoteRepository defaultRepo = proxyRepoIfProxyEnabled(new RemoteRepository.Builder(id, DEFAULT_CONTENT_TYPE, url).build());
defaultRepos.add(defaultRepo);
});
this.remoteRepositories.addAll(0, defaultRepos);
}
if (logger.isDebugEnabled()) {
logger.debug("Using remote repositories: {}", actualRemoteRepositoriesDescription());
}
this.repositorySystem = newRepositorySystem();
}
/**
* Gets the default repos to automatically add.
* @return map of default repos (repo url to repo id)
*/
protected Map<String, String> defaultRemoteRepos() {
Map<String, String> defaultRepos = new LinkedHashMap<>();
defaultRepos.put("https://repo.maven.apache.org/maven2", "mavenCentral-default");
defaultRepos.put("https://repo.spring.io/snapshot", "springSnapshot-default");
defaultRepos.put("https://repo.spring.io/milestone", "springMilestone-default");
return defaultRepos;
}
private RemoteRepository proxyRepoIfProxyEnabled(RemoteRepository remoteRepo) {
if (!isProxyEnabled()) {
return remoteRepo;
}
Proxy proxy;
MavenProperties.Proxy proxyProperties = this.properties.getProxy();
if (this.proxyAuthentication != null) {
proxy = new Proxy(
proxyProperties.getProtocol(),
proxyProperties.getHost(),
proxyProperties.getPort(),
this.proxyAuthentication);
}
else {
// if proxy does not require authentication
proxy = new Proxy(
proxyProperties.getProtocol(),
proxyProperties.getHost(),
proxyProperties.getPort());
}
DefaultProxySelector proxySelector = new DefaultProxySelector();
proxySelector.add(proxy, this.properties.getProxy().getNonProxyHosts());
proxy = proxySelector.getProxy(remoteRepo);
RemoteRepository.Builder remoteRepositoryBuilder = new RemoteRepository.Builder(remoteRepo);
remoteRepositoryBuilder.setProxy(proxy);
return remoteRepositoryBuilder.build();
}
/**
* Check if the proxy settings are provided.
*
* @return boolean true if the proxy settings are provided.
*/
private boolean isProxyEnabled() {
return (this.properties.getProxy() != null &&
this.properties.getProxy().getHost() != null &&
this.properties.getProxy().getPort() > 0);
}
/**
* Check if the proxy setting has username/password set.
*
* @return boolean true if both the username/password are set
*/
private boolean proxyHasCredentials() {
return (this.properties.getProxy() != null &&
this.properties.getProxy().getAuth() != null &&
this.properties.getProxy().getAuth().getUsername() != null &&
this.properties.getProxy().getAuth().getPassword() != null);
}
/**
* Check if the {@link MavenProperties.RemoteRepository} setting has username/password set.
*
* @return boolean true if both the username/password are set
*/
private boolean remoteRepositoryHasCredentials(MavenProperties.RemoteRepository remoteRepository) {
return remoteRepository != null &&
remoteRepository.getAuth() != null &&
remoteRepository.getAuth().getUsername() != null &&
remoteRepository.getAuth().getPassword() != null;
}
/**
* Create an {@link Authentication} given a username/password.
*
* @param username the user
* @param password the password
* @return a configured {@link Authentication}
*/
private Authentication newAuthentication(final String username, final String password) {
return new Authentication() {
@Override
public void fill(AuthenticationContext context, String key, Map<String, String> data) {
context.put(AuthenticationContext.USERNAME, username);
context.put(AuthenticationContext.PASSWORD, password);
}
@Override
public void digest(AuthenticationDigest digest) {
digest.update(AuthenticationContext.USERNAME, username,
AuthenticationContext.PASSWORD, password);
}
};
}
DefaultRepositorySystemSession newRepositorySystemSession() {
return this.newRepositorySystemSession(this.repositorySystem, this.properties.getLocalRepository());
}
/*
* Create a session to manage remote and local synchronization.
*/
private DefaultRepositorySystemSession newRepositorySystemSession(RepositorySystem system, String localRepoPath) {
DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
LocalRepository localRepo = new LocalRepository(localRepoPath);
session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo));
session.setOffline(this.properties.isOffline());
session.setUpdatePolicy(this.properties.getUpdatePolicy());
session.setChecksumPolicy(this.properties.getChecksumPolicy());
if (this.properties.isEnableRepositoryListener()) {
session.setRepositoryListener(new LoggingRepositoryListener());
}
if (this.properties.getConnectTimeout() != null) {
session.setConfigProperty(ConfigurationProperties.CONNECT_TIMEOUT, this.properties.getConnectTimeout());
}
if (this.properties.getRequestTimeout() != null) {
session.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, this.properties.getRequestTimeout());
}
if (isProxyEnabled()) {
DefaultProxySelector proxySelector = new DefaultProxySelector();
Proxy proxy = new Proxy(this.properties.getProxy().getProtocol(),
this.properties.getProxy().getHost(),
this.properties.getProxy().getPort(),
this.proxyAuthentication);
proxySelector.add(proxy, this.properties.getProxy().getNonProxyHosts());
session.setProxySelector(proxySelector);
}
// wagon configs
for (Entry<String, MavenProperties.RemoteRepository> entry : this.properties.getRemoteRepositories().entrySet()) {
session.setConfigProperty("aether.connector.wagon.config." + entry.getKey(), entry.getValue().getWagon());
}
return session;
}
/*
* Aether's components implement {@link org.eclipse.aether.spi.locator.Service} to ease manual wiring.
* Using the prepopulated {@link DefaultServiceLocator}, we need to register the repository connector
* and transporter factories
*/
private RepositorySystem newRepositorySystem() {
DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator();
locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class);
locator.addService(TransporterFactory.class, FileTransporterFactory.class);
locator.addService(TransporterFactory.class, HttpTransporterFactory.class);
locator.setErrorHandler(new DefaultServiceLocator.ErrorHandler() {
@Override
public void serviceCreationFailed(Class<?> type, Class<?> impl, Throwable exception) {
throw new RuntimeException(exception);
}
});
return locator.getService(RepositorySystem.class);
}
/**
* Gets the list of configured remote repositories.
* @return unmodifiable list of configured remote repositories.
*/
List<RemoteRepository> remoteRepositories() {
return Collections.unmodifiableList(this.remoteRepositories);
}
private String actualRemoteRepositoriesDescription() {
return this.remoteRepositories.stream().map((repo) -> String.format("%s (%s)", repo.getId(), repo.getUrl()))
.collect(Collectors.joining(", ", "[", "]"));
}
private String configuredRemoteRepositoriesDescription() {
return this.properties.getRemoteRepositories().entrySet().stream()
.map((e) -> String.format("%s (%s)", e.getKey(), e.getValue().getUrl()))
.collect(Collectors.joining(", ", "[", "]"));
}
List<String> getVersions(String coordinates) {
Artifact artifact = new DefaultArtifact(coordinates);
VersionRangeRequest rangeRequest = new VersionRangeRequest();
rangeRequest.setArtifact(artifact);
rangeRequest.setRepositories(this.remoteRepositories);
try {
VersionRangeResult versionResult = this.repositorySystem.resolveVersionRange(newRepositorySystemSession(), rangeRequest);
List<String> versions = new ArrayList<>();
for (Version version: versionResult.getVersions()) {
versions.add(version.toString());
}
return versions;
}
catch (VersionRangeResolutionException e) {
throw new IllegalStateException(e);
}
}
/**
* Resolve an artifact and return its location in the local repository. Aether performs the normal
* Maven resolution process ensuring that the latest update is cached to the local repository.
* In addition, if the {@code MavenProperties.resolvePom} flag is <code>true</code>,
* the POM is also resolved and cached.
* @param resource the {@link MavenResource} representing the artifact
* @return a {@link FileSystemResource} representing the resolved artifact in the local repository
* @throws IllegalStateException if the artifact does not exist or the resolution fails
*/
Resource resolve(MavenResource resource) {
Assert.notNull(resource, "MavenResource must not be null");
validateCoordinates(resource);
RepositorySystemSession session = newRepositorySystemSession(this.repositorySystem, this.properties.getLocalRepository());
try {
List<ArtifactRequest> artifactRequests = new ArrayList<>(2);
if (properties.isResolvePom()) {
artifactRequests.add(new ArtifactRequest(toPomArtifact(resource), this.remoteRepositories, JavaScopes.RUNTIME));
}
artifactRequests.add(new ArtifactRequest(toJarArtifact(resource), this.remoteRepositories, JavaScopes.RUNTIME));
List<ArtifactResult> results = this.repositorySystem.resolveArtifacts(session, artifactRequests);
return toResource(results.get(results.size() - 1));
}
catch (ArtifactResolutionException ex) {
String errorMsg = String.format("Failed to resolve %s using remote repo(s): %s",
resource, actualRemoteRepositoriesDescription());
throw new IllegalStateException(errorMsg, ex);
}
}
private void validateCoordinates(MavenResource resource) {
Assert.hasText(resource.getGroupId(), "groupId must not be blank.");
Assert.hasText(resource.getArtifactId(), "artifactId must not be blank.");
Assert.hasText(resource.getExtension(), "extension must not be blank.");
Assert.hasText(resource.getVersion(), "version must not be blank.");
}
public FileSystemResource toResource(ArtifactResult resolvedArtifact) {
return new FileSystemResource(resolvedArtifact.getArtifact().getFile());
}
private Artifact toJarArtifact(MavenResource resource) {
return toArtifact(resource, resource.getExtension());
}
private Artifact toPomArtifact(MavenResource resource) {
return toArtifact(resource, "pom");
}
private Artifact toArtifact(MavenResource resource, String extension) {
return new DefaultArtifact(resource.getGroupId(),
resource.getArtifactId(),
resource.getClassifier() != null ? resource.getClassifier() : "",
extension,
resource.getVersion());
}
}

View File

@@ -0,0 +1,491 @@
/*
* Copyright 2019-2025 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.function.deployer.utils;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
/**
* Configuration Properties for Maven.
*
* @author Ilayaperumal Gopinathan
* @author Eric Bottard
* @author Mark Fisher
* @author Donovan Muller
*/
public class MavenProperties {
/**
* Default file path to a locally available maven repository.
*/
private static String DEFAULT_LOCAL_REPO = System.getProperty("user.home") +
File.separator + ".m2" + File.separator + "repository";
/**
* Whether default remote repositories should be automatically included in the list of remote repositories.
*/
private boolean includeDefaultRemoteRepos = true;
/**
* File path to a locally available maven repository, where artifacts will be downloaded.
*/
private String localRepository = DEFAULT_LOCAL_REPO;
/**
* Locations of remote maven repositories from which artifacts will be downloaded, if not available locally.
*/
private Map<String, RemoteRepository> remoteRepositories = new TreeMap<>();
/**
* Whether the resolver should operate in offline mode.
*/
private boolean offline;
/**
* Proxy configuration properties.
*/
private Proxy proxy;
/**
* The connect timeout. If <code>null</code>, the underlying default will be used.
*/
private Integer connectTimeout;
/**
* The request timeout. If <code>null</code>, the underlying default will be used.
*/
private Integer requestTimeout;
/**
* In addition to resolving the JAR artifact, if true, resolve the POM artifact.
* This is consistent with the way that Maven resolves artifacts.
*/
private boolean resolvePom;
private String updatePolicy;
private String checksumPolicy;
/**
* Add the ConsoleRepositoryListener to the session for debugging of artifact resolution.
*/
private boolean enableRepositoryListener = false;
boolean isIncludeDefaultRemoteRepos() {
return includeDefaultRemoteRepos;
}
void setIncludeDefaultRemoteRepos(boolean includeDefaultRemoteRepos) {
this.includeDefaultRemoteRepos = includeDefaultRemoteRepos;
}
/**
* Use maven wagon based transport for http based artifacts.
*/
private boolean useWagon;
public void setUseWagon(boolean useWagon) {
this.useWagon = useWagon;
}
public boolean isUseWagon() {
return useWagon;
}
public boolean isEnableRepositoryListener() {
return enableRepositoryListener;
}
public void setEnableRepositoryListener(boolean enableRepositoryListener) {
this.enableRepositoryListener = enableRepositoryListener;
}
public String getUpdatePolicy() {
return updatePolicy;
}
public void setUpdatePolicy(String updatePolicy) {
this.updatePolicy = updatePolicy;
}
public String getChecksumPolicy() {
return checksumPolicy;
}
public void setChecksumPolicy(String checksumPolicy) {
this.checksumPolicy = checksumPolicy;
}
public Map<String, RemoteRepository> getRemoteRepositories() {
return remoteRepositories;
}
public void setRemoteRepositories(final Map<String, RemoteRepository> remoteRepositories) {
this.remoteRepositories = new TreeMap<>(remoteRepositories);
}
public void setLocalRepository(String localRepository) {
this.localRepository = localRepository;
}
public String getLocalRepository() {
return localRepository;
}
public boolean isOffline() {
return offline;
}
public void setOffline(Boolean offline) {
this.offline = offline;
}
public Integer getConnectTimeout() {
return this.connectTimeout;
}
public void setConnectTimeout(Integer connectTimeout) {
this.connectTimeout = connectTimeout;
}
public Integer getRequestTimeout() {
return this.requestTimeout;
}
public void setRequestTimeout(Integer requestTimeout) {
this.requestTimeout = requestTimeout;
}
public Proxy getProxy() {
return this.proxy;
}
public void setProxy(Proxy proxy) {
this.proxy = proxy;
}
public boolean isResolvePom() {
return resolvePom;
}
public void setResolvePom(final boolean resolvePom) {
this.resolvePom = resolvePom;
}
public static class Proxy {
/**
* Protocol to use for proxy settings.
*/
private String protocol = "http";
/**
* Host for the proxy.
*/
private String host;
/**
* Port for the proxy.
*/
private int port;
/**
* List of non proxy hosts.
*/
private String nonProxyHosts;
private Authentication auth;
public String getProtocol() {
return this.protocol;
}
public void setProtocol(String protocol) {
this.protocol = protocol;
}
public String getHost() {
return this.host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return this.port;
}
public void setPort(int port) {
this.port = port;
}
public String getNonProxyHosts() {
return this.nonProxyHosts;
}
public void setNonProxyHosts(String nonProxyHosts) {
this.nonProxyHosts = nonProxyHosts;
}
public Authentication getAuth() {
return this.auth;
}
public void setAuth(Authentication auth) {
this.auth = auth;
}
}
public enum WagonHttpMethod {
// directly maps to http methods in org.apache.maven.wagon.shared.http.HttpConfiguration
/**
* All methods.
*/
all,
/**
* GET method.
*/
get,
/**
* PUT method.
*/
put,
/**
* HEAD method.
*/
head;
}
public static class WagonHttpMethodProperties {
// directly maps to settings in org.apache.maven.wagon.shared.http.HttpMethodConfiguration
private boolean usePreemptive;
private boolean useDefaultHeaders;
private Integer connectionTimeout;
private Integer readTimeout;
private Map<String, String> headers = new HashMap<>();
private Map<String, String> params = new HashMap<>();
public boolean isUsePreemptive() {
return usePreemptive;
}
public void setUsePreemptive(boolean usePreemptive) {
this.usePreemptive = usePreemptive;
}
public boolean isUseDefaultHeaders() {
return useDefaultHeaders;
}
public void setUseDefaultHeaders(boolean useDefaultHeaders) {
this.useDefaultHeaders = useDefaultHeaders;
}
public Integer getConnectionTimeout() {
return connectionTimeout;
}
public void setConnectionTimeout(Integer connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}
public Integer getReadTimeout() {
return readTimeout;
}
public void setReadTimeout(Integer readTimeout) {
this.readTimeout = readTimeout;
}
public Map<String, String> getHeaders() {
return headers;
}
public void setHeaders(Map<String, String> headers) {
this.headers = headers;
}
public Map<String, String> getParams() {
return params;
}
public void setParams(Map<String, String> params) {
this.params = params;
}
}
public static class Wagon {
private Map<WagonHttpMethod, WagonHttpMethodProperties> http = new HashMap<>();
public Map<WagonHttpMethod, WagonHttpMethodProperties> getHttp() {
return http;
}
public void setHttp(Map<WagonHttpMethod, WagonHttpMethodProperties> http) {
this.http = http;
}
}
public static class RemoteRepository {
/**
* URL of the remote maven repository. E.g. https://my.repo.com
*/
private String url;
private Authentication auth;
private RepositoryPolicy policy;
private RepositoryPolicy snapshotPolicy;
private RepositoryPolicy releasePolicy;
private Wagon wagon = new Wagon();
public RemoteRepository() {
}
public RemoteRepository(final String url) {
this.url = url;
}
public RemoteRepository(final String url, final Authentication auth) {
this.url = url;
this.auth = auth;
}
public Wagon getWagon() {
return wagon;
}
public void setWagon(Wagon wagon) {
this.wagon = wagon;
}
public String getUrl() {
return url;
}
public void setUrl(final String url) {
this.url = url;
}
public Authentication getAuth() {
return auth;
}
public void setAuth(final Authentication auth) {
this.auth = auth;
}
public RepositoryPolicy getPolicy() {
return policy;
}
public void setPolicy(RepositoryPolicy policy) {
this.policy = policy;
}
public RepositoryPolicy getSnapshotPolicy() {
return snapshotPolicy;
}
public void setSnapshotPolicy(RepositoryPolicy snapshotPolicy) {
this.snapshotPolicy = snapshotPolicy;
}
public RepositoryPolicy getReleasePolicy() {
return releasePolicy;
}
public void setReleasePolicy(RepositoryPolicy releasePolicy) {
this.releasePolicy = releasePolicy;
}
}
public static class RepositoryPolicy {
private boolean enabled = true;
private String updatePolicy = "always";
private String checksumPolicy = "warn";
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getUpdatePolicy() {
return updatePolicy;
}
public void setUpdatePolicy(String updatePolicy) {
this.updatePolicy = updatePolicy;
}
public String getChecksumPolicy() {
return checksumPolicy;
}
public void setChecksumPolicy(String checksumPolicy) {
this.checksumPolicy = checksumPolicy;
}
}
public static class Authentication {
private String username;
private String password;
public Authentication() {
}
public Authentication(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return this.password;
}
public void setPassword(String password) {
this.password = password;
}
}
}

View File

@@ -0,0 +1,324 @@
/*
* Copyright 2019-2025 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.function.deployer.utils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* A {@link Resource} implementation for resolving an artifact via maven coordinates.
* <p>
* The {@code MavenResource} class contains <a href="https://maven.apache.org/pom.html#Maven_Coordinates">
* Maven coordinates</a> for a jar file containing an app/library, or a Bill of Materials pom.
* <p>
* To create a new instance, either use {@link Builder} to set the individual fields:
* <pre>
* new MavenResource.Builder()
* .setGroupId("org.springframework.sample")
* .setArtifactId("some-app")
* .setExtension("jar") //optional
* .setClassifier("exec") //optional
* .setVersion("2.0.0")
* .build()
* </pre>
* ...or use {@link #parse(String)} to parse the coordinates as a colon delimited string:
* <code>&lt;groupId&gt;:&lt;artifactId&gt;[:&lt;extension&gt;[:&lt;classifier&gt;]]:&lt;version&gt;</code>
* <pre>
* MavenResource.parse("org.springframework.sample:some-app:2.0.0);
* MavenResource.parse("org.springframework.sample:some-app:jar:exec:2.0.0);
* </pre>
* @author David Turanski
* @author Mark Fisher
* @author Patrick Peralta
* @author Venil Noronha
* @author Ilayaperumal Gopinathan
*/
public final class MavenResource extends AbstractResource {
/**
* URI Scheme.
*/
public static String URI_SCHEME = "maven";
/**
* The default extension for the artifact.
*/
final static String DEFAULT_EXTENSION = "jar";
/**
* String representing an empty classifier.
*/
final static String EMPTY_CLASSIFIER = "";
/**
* Group ID for artifact; generally this includes the name of the
* organization that generated the artifact.
*/
private final String groupId;
/**
* Artifact ID; generally this includes the name of the app or library.
*/
private final String artifactId;
/**
* Extension of the artifact.
*/
private final String extension;
/**
* Classifier of the artifact.
*/
private final String classifier;
/**
* Version of the artifact.
*/
private final String version;
private final MavenArtifactResolver resolver;
/**
* Construct a {@code MavenResource} object.
*
* @param groupId group ID for artifact
* @param artifactId artifact ID
* @param extension the file extension
* @param classifier artifact classifier - can be null
* @param version artifact version
* @param properties Maven configuration properties
*/
private MavenResource(String groupId, String artifactId, String extension, String classifier,
String version, MavenProperties properties) {
Assert.hasText(groupId, "groupId must not be blank");
Assert.hasText(artifactId, "artifactId must not be blank");
Assert.hasText(extension, "extension must not be blank");
Assert.hasText(version, "version must not be blank");
this.groupId = groupId;
this.artifactId = artifactId;
this.extension = extension;
this.classifier = classifier == null ? EMPTY_CLASSIFIER : classifier;
this.version = version;
this.resolver = new MavenArtifactResolver(properties != null ? properties : new MavenProperties());
}
public String getGroupId() {
return groupId;
}
public String getArtifactId() {
return artifactId;
}
public String getExtension() {
return extension;
}
public String getClassifier() {
return classifier;
}
public String getVersion() {
return version;
}
@Override
public String getDescription() {
return this.toString();
}
@Override
public InputStream getInputStream() throws IOException {
return resolver.resolve(this).getInputStream();
}
@Override
public File getFile() throws IOException {
return resolver.resolve(this).getFile();
}
@Override
public String getFilename() {
return StringUtils.hasLength(classifier) ?
String.format("%s-%s-%s.%s", artifactId, version, classifier, extension) :
String.format("%s-%s.%s", artifactId, version, extension);
}
@Override
public boolean exists() {
try {
return super.exists();
}
catch (Exception e) {
// Resource.exists() has no throws clause, so return false
return false;
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof MavenResource)) {
return false;
}
MavenResource that = (MavenResource) o;
return this.groupId.equals(that.groupId) &&
this.artifactId.equals(that.artifactId) &&
this.extension.equals(that.extension) &&
this.classifier.equals(that.classifier) &&
this.version.equals(that.version);
}
@Override
public int hashCode() {
int result = groupId.hashCode();
result = 31 * result + artifactId.hashCode();
result = 31 * result + extension.hashCode();
if (StringUtils.hasLength(classifier)) {
result = 31 * result + classifier.hashCode();
}
result = 31 * result + version.hashCode();
return result;
}
/**
* Returns the coordinates encoded as
* &lt;groupId&gt;:&lt;artifactId&gt;[:&lt;extension&gt;[:&lt;classifier&gt;]]:&lt;version&gt;,
* conforming to the <a href="https://www.eclipse.org/aether">Aether</a> convention.
*/
@Override
public String toString() {
return StringUtils.hasLength(classifier) ?
String.format("%s:%s:%s:%s:%s", groupId, artifactId, extension, classifier, version) :
String.format("%s:%s:%s:%s", groupId, artifactId, extension, version);
}
@Override
public URI getURI() throws IOException {
return URI.create(URI_SCHEME + "://" + toString());
}
/**
* Create a {@link MavenResource} for the provided coordinates and default properties.
*
* @param coordinates coordinates encoded as &lt;groupId&gt;:&lt;artifactId&gt;[:&lt;extension&gt;[:&lt;classifier&gt;]]:&lt;version&gt;,
* conforming to the <a href="https://www.eclipse.org/aether">Aether</a> convention.
* @return the {@link MavenResource}
*/
public static MavenResource parse(String coordinates) {
return parse(coordinates, null);
}
/**
* Create a {@link MavenResource} for the provided coordinates and properties.
*
* @param coordinates coordinates encoded as &lt;groupId&gt;:&lt;artifactId&gt;[:&lt;extension&gt;[:&lt;classifier&gt;]]:&lt;version&gt;,
* conforming to the <a href="https://www.eclipse.org/aether">Aether</a> convention.
* @param properties the properties for the repositories, proxies, and authentication
* @return the {@link MavenResource}
*/
public static MavenResource parse(String coordinates, MavenProperties properties) {
Assert.hasText(coordinates, "coordinates are required");
Pattern p = Pattern.compile("([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)");
Matcher m = p.matcher(coordinates);
Assert.isTrue(m.matches(), "Bad artifact coordinates " + coordinates
+ ", expected format is <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>");
String groupId = m.group(1);
String artifactId = m.group(2);
String extension = StringUtils.hasLength(m.group(4)) ? m.group(4) : DEFAULT_EXTENSION;
String classifier = StringUtils.hasLength(m.group(6)) ? m.group(6) : EMPTY_CLASSIFIER;
String version = m.group(7);
return new MavenResource(groupId, artifactId, extension, classifier, version, properties);
}
/**
* Get all the available versions on this maven co-ordinate.
* @param coordinates the co-ordinate with the version constraint added.
* Example: org.springframework.cloud.stream.app:http-source-rabbit:[0,)
* @return the list of all the available versions
*/
public List<String> getVersions(String coordinates) {
return this.resolver.getVersions(coordinates);
}
public static class Builder {
private String groupId;
private String artifactId;
private String extension = DEFAULT_EXTENSION;
private String classifier = EMPTY_CLASSIFIER;
private String version;
private final MavenProperties properties;
public Builder() {
this(null);
}
public Builder(MavenProperties properties) {
this.properties = properties;
}
public Builder groupId(String groupId) {
this.groupId = groupId;
return this;
}
public Builder artifactId(String artifactId) {
this.artifactId = artifactId;
return this;
}
public Builder extension(String extension) {
this.extension = extension;
return this;
}
public Builder classifier(String classifier) {
this.classifier = classifier;
return this;
}
public Builder version(String version) {
this.version = version;
return this;
}
public MavenResource build() {
return new MavenResource(groupId, artifactId, extension, classifier, version, properties);
}
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2019-2025 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.cloud.function.deployer.utils;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* A {@link ResourceLoader} that loads {@link MavenResource}s from locations of the format
* {@literal maven://<coordinates>} where the value for "coordinates" conforms to the rules
* described on {@link MavenResource#parse(String)} .
*
* @author Mark Fisher
*/
public class MavenResourceLoader implements ResourceLoader {
private static final String URI_SCHEME = "maven";
private final MavenProperties properties;
private final ClassLoader classLoader = ClassUtils.getDefaultClassLoader();
/**
* Create a {@link MavenResourceLoader} that uses the provided {@link MavenProperties}.
*
* @param properties the {@link MavenProperties} to use when instantiating {@link MavenResource}s
*/
public MavenResourceLoader(MavenProperties properties) {
Assert.notNull(properties, "MavenProperties must not be null");
this.properties = properties;
}
/**
* Returns a {@link MavenResource} for the provided location.
*
* @param location the coordinates conforming to the rules described on
* {@link MavenResource#parse(String)}. May optionally be preceded by {@value #URI_SCHEME}
* followed by a colon and zero or more forward slashes, e.g.
* {@literal maven://group:artifact:version}
* @return the {@link MavenResource}
*/
@Override
public Resource getResource(String location) {
Assert.hasText(location, "location is required");
String coordinates = location.replaceFirst(URI_SCHEME + ":\\/*", "");
return MavenResource.parse(coordinates, this.properties);
}
/**
* Returns the {@link ClassLoader} for this ResourceLoader.
*/
@Override
public ClassLoader getClassLoader() {
return this.classLoader;
}
}

View File

@@ -32,9 +32,9 @@ import reactor.util.function.Tuples;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.deployer.resource.maven.MavenProperties;
import org.springframework.cloud.function.cloudevent.CloudEventMessageBuilder;
import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.cloud.function.deployer.utils.MavenProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;