diff --git a/spring-cloud-function-deployer/pom.xml b/spring-cloud-function-deployer/pom.xml index 52a8759a2..c600d5cbf 100644 --- a/spring-cloud-function-deployer/pom.xml +++ b/spring-cloud-function-deployer/pom.xml @@ -15,6 +15,10 @@ 17 + 2.16.1 + 3.9.6 + 1.9.18 + 3.5.3 @@ -34,11 +38,7 @@ org.springframework.cloud spring-cloud-function-context - - org.springframework.cloud - spring-cloud-deployer-resource-maven - 2.5.1 - + org.springframework.boot spring-boot-starter-test @@ -55,6 +55,66 @@ 2.2.0 test + + + org.apache.maven + maven-model-builder + ${maven.version} + + + + javax.inject + javax.inject + + + + + org.apache.maven + maven-resolver-provider + ${maven.version} + + + + javax.inject + javax.inject + + + + + org.apache.maven.resolver + maven-resolver-connector-basic + ${maven-resolver.version} + + + org.apache.maven.resolver + maven-resolver-transport-file + ${maven-resolver.version} + + + org.apache.maven.resolver + maven-resolver-transport-http + ${maven-resolver.version} + + + org.apache.maven.resolver + maven-resolver-transport-wagon + ${maven-resolver.version} + + + org.apache.maven.resolver + maven-resolver-impl + ${maven-resolver.version} + + + commons-io + commons-io + 2.19.0 + + + org.apache.maven.wagon + wagon-http + 3.5.3 + diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java index 2105c5b0d..607b5e60f 100644 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java @@ -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; diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/LoggingRepositoryListener.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/LoggingRepositoryListener.java new file mode 100644 index 000000000..8a6d2ce2d --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/LoggingRepositoryListener.java @@ -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); + } +} diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenArtifactResolver.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenArtifactResolver.java new file mode 100644 index 000000000..fe3de5a8d --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenArtifactResolver.java @@ -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. + *

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 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 defaultRepoUrlsToIds = defaultRemoteRepos(); + + for (Map.Entry 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 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 defaultRemoteRepos() { + Map 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 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 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 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 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 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 true, + * 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 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 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()); + } +} diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenProperties.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenProperties.java new file mode 100644 index 000000000..4abc9b464 --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenProperties.java @@ -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 remoteRepositories = new TreeMap<>(); + + /** + * Whether the resolver should operate in offline mode. + */ + private boolean offline; + + /** + * Proxy configuration properties. + */ + private Proxy proxy; + + /** + * The connect timeout. If null, the underlying default will be used. + */ + private Integer connectTimeout; + + /** + * The request timeout. If null, 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 getRemoteRepositories() { + return remoteRepositories; + } + + public void setRemoteRepositories(final Map 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 headers = new HashMap<>(); + private Map 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 getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public Map getParams() { + return params; + } + + public void setParams(Map params) { + this.params = params; + } + } + + public static class Wagon { + + private Map http = new HashMap<>(); + + public Map getHttp() { + return http; + } + + public void setHttp(Map 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; + } + + } +} + diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenResource.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenResource.java new file mode 100644 index 000000000..95799a320 --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenResource.java @@ -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. + *

+ * The {@code MavenResource} class contains + * Maven coordinates for a jar file containing an app/library, or a Bill of Materials pom. + *

+ * To create a new instance, either use {@link Builder} to set the individual fields: + *

+ * new MavenResource.Builder()
+ *     .setGroupId("org.springframework.sample")
+ *     .setArtifactId("some-app")
+ *     .setExtension("jar") //optional
+ *     .setClassifier("exec") //optional
+ *     .setVersion("2.0.0")
+ *     .build()
+ * 
+ * ...or use {@link #parse(String)} to parse the coordinates as a colon delimited string: + * <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version> + *
+ * MavenResource.parse("org.springframework.sample:some-app:2.0.0);
+ * MavenResource.parse("org.springframework.sample:some-app:jar:exec:2.0.0);
+ * 
+ * @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 + * <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>, + * conforming to the Aether 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 <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>, + * conforming to the Aether 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 <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>, + * conforming to the Aether 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 :[:[:]]:"); + 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 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); + } + } +} + diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenResourceLoader.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenResourceLoader.java new file mode 100644 index 000000000..41e25ef0f --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenResourceLoader.java @@ -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://} 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; + } + +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java index e3317a4d0..8074c7dc2 100644 --- a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java @@ -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;