Commit 84937551 authored by Andy Wilkinson's avatar Andy Wilkinson

Configure CLI with repositories from active profiles in settings.xml

This commit enhances the CLI to use the repositories configured in the
profiles declared in a user's Maven settings.xml file during
dependency resolution. A profile must be active for its repositories
to be used.

Closes gh-2703
Closes gh-3483
parent eafee1ec
/*
* Copyright 2012-2015 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import java.io.File;
import java.lang.reflect.Field;
import org.apache.maven.settings.Settings;
import org.apache.maven.settings.building.DefaultSettingsBuilderFactory;
import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
import org.apache.maven.settings.building.SettingsBuildingException;
import org.apache.maven.settings.building.SettingsBuildingRequest;
import org.apache.maven.settings.crypto.DefaultSettingsDecrypter;
import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
import org.apache.maven.settings.crypto.SettingsDecrypter;
import org.apache.maven.settings.crypto.SettingsDecryptionResult;
import org.sonatype.plexus.components.cipher.DefaultPlexusCipher;
import org.sonatype.plexus.components.cipher.PlexusCipherException;
import org.sonatype.plexus.components.sec.dispatcher.DefaultSecDispatcher;
import org.springframework.boot.cli.util.Log;
/**
* {@code MavenSettingsReader} reads settings from a user's Maven settings.xml file,
* decrypting them if necessary using settings-security.xml.
*
* @author Andy Wilkinson
* @since 1.3.0
*/
public class MavenSettingsReader {
private final String homeDir;
public MavenSettingsReader() {
this(System.getProperty("user.home"));
}
public MavenSettingsReader(String homeDir) {
this.homeDir = homeDir;
}
public MavenSettings readSettings() {
Settings settings = loadSettings();
SettingsDecryptionResult decrypted = decryptSettings(settings);
if (!decrypted.getProblems().isEmpty()) {
Log.error("Maven settings decryption failed. Some Maven repositories may be inaccessible");
// Continue - the encrypted credentials may not be used
}
return new MavenSettings(settings, decrypted);
}
private Settings loadSettings() {
File settingsFile = new File(this.homeDir, ".m2/settings.xml");
SettingsBuildingRequest request = new DefaultSettingsBuildingRequest();
request.setUserSettingsFile(settingsFile);
request.setSystemProperties(System.getProperties());
try {
return new DefaultSettingsBuilderFactory().newInstance().build(request)
.getEffectiveSettings();
}
catch (SettingsBuildingException ex) {
throw new IllegalStateException("Failed to build settings from "
+ settingsFile, ex);
}
}
private SettingsDecryptionResult decryptSettings(Settings settings) {
DefaultSettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest(
settings);
return createSettingsDecrypter().decrypt(request);
}
private SettingsDecrypter createSettingsDecrypter() {
SettingsDecrypter settingsDecrypter = new DefaultSettingsDecrypter();
setField(DefaultSettingsDecrypter.class, "securityDispatcher", settingsDecrypter,
new SpringBootSecDispatcher());
return settingsDecrypter;
}
private void setField(Class<?> clazz, String fieldName, Object target, Object value) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
catch (Exception e) {
throw new IllegalStateException("Failed to set field '" + fieldName
+ "' on '" + target + "'", e);
}
}
private class SpringBootSecDispatcher extends DefaultSecDispatcher {
public SpringBootSecDispatcher() {
this._configurationFile = new File(MavenSettingsReader.this.homeDir,
".m2/settings-security.xml").getAbsolutePath();
try {
this._cipher = new DefaultPlexusCipher();
}
catch (PlexusCipherException e) {
throw new IllegalStateException(e);
}
}
}
}
......@@ -21,11 +21,8 @@ import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import org.apache.maven.settings.Settings;
import org.apache.maven.settings.building.DefaultSettingsBuilderFactory;
import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
import org.apache.maven.settings.building.SettingsBuildingException;
import org.apache.maven.settings.building.SettingsBuildingRequest;
import org.apache.maven.settings.Profile;
import org.apache.maven.settings.Repository;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
import org.springframework.util.StringUtils;
......@@ -46,58 +43,54 @@ public final class RepositoryConfigurationFactory {
private static final RepositoryConfiguration SPRING_SNAPSHOT = new RepositoryConfiguration(
"spring-snapshot", URI.create("http://repo.spring.io/snapshot"), true);
private RepositoryConfigurationFactory() {
}
/**
* @return the newly-created default repository configuration
*/
public static List<RepositoryConfiguration> createDefaultRepositoryConfiguration() {
MavenSettings mavenSettings = new MavenSettingsReader().readSettings();
List<RepositoryConfiguration> repositoryConfiguration = new ArrayList<RepositoryConfiguration>();
repositoryConfiguration.add(MAVEN_CENTRAL);
if (!Boolean.getBoolean("disableSpringSnapshotRepos")) {
repositoryConfiguration.add(SPRING_MILESTONE);
repositoryConfiguration.add(SPRING_SNAPSHOT);
}
addDefaultCacheAsRepository(repositoryConfiguration);
addDefaultCacheAsRepository(mavenSettings.getLocalRepository(),
repositoryConfiguration);
addActiveProfileRepositories(mavenSettings.getActiveProfiles(),
repositoryConfiguration);
return repositoryConfiguration;
}
/**
* Add the default local M2 cache directory as a remote repository. Only do this if
* the local cache location has been changed from the default.
* @param repositoryConfiguration
*/
public static void addDefaultCacheAsRepository(
private static void addDefaultCacheAsRepository(String localRepository,
List<RepositoryConfiguration> repositoryConfiguration) {
RepositoryConfiguration repository = new RepositoryConfiguration("local",
getLocalRepositoryDirectory().toURI(), true);
getLocalRepositoryDirectory(localRepository).toURI(), true);
if (!repositoryConfiguration.contains(repository)) {
repositoryConfiguration.add(0, repository);
}
}
private static File getLocalRepositoryDirectory() {
String localRepository = loadSettings().getLocalRepository();
if (StringUtils.hasText(localRepository)) {
return new File(localRepository);
private static void addActiveProfileRepositories(List<Profile> activeProfiles,
List<RepositoryConfiguration> repositoryConfiguration) {
for (Profile activeProfile : activeProfiles) {
for (Repository repository : activeProfile.getRepositories()) {
repositoryConfiguration.add(new RepositoryConfiguration(repository
.getId(), URI.create(repository.getUrl()), repository
.getSnapshots() != null ? repository.getSnapshots().isEnabled()
: false));
}
}
return new File(getM2HomeDirectory(), "repository");
}
private static Settings loadSettings() {
File settingsFile = new File(System.getProperty("user.home"), ".m2/settings.xml");
SettingsBuildingRequest request = new DefaultSettingsBuildingRequest();
request.setUserSettingsFile(settingsFile);
request.setSystemProperties(System.getProperties());
try {
return new DefaultSettingsBuilderFactory().newInstance().build(request)
.getEffectiveSettings();
}
catch (SettingsBuildingException ex) {
throw new IllegalStateException("Failed to build settings from "
+ settingsFile, ex);
private static File getLocalRepositoryDirectory(String localRepository) {
if (StringUtils.hasText(localRepository)) {
return new File(localRepository);
}
return new File(getM2HomeDirectory(), "repository");
}
private static File getM2HomeDirectory() {
......
......@@ -16,38 +16,11 @@
package org.springframework.boot.cli.compiler.grape;
import java.io.File;
import java.lang.reflect.Field;
import java.util.List;
import org.apache.maven.settings.Mirror;
import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Server;
import org.apache.maven.settings.Settings;
import org.apache.maven.settings.building.DefaultSettingsBuilderFactory;
import org.apache.maven.settings.building.DefaultSettingsBuildingRequest;
import org.apache.maven.settings.building.SettingsBuildingException;
import org.apache.maven.settings.building.SettingsBuildingRequest;
import org.apache.maven.settings.crypto.DefaultSettingsDecrypter;
import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest;
import org.apache.maven.settings.crypto.SettingsDecrypter;
import org.apache.maven.settings.crypto.SettingsDecryptionResult;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.repository.Authentication;
import org.eclipse.aether.repository.AuthenticationSelector;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.MirrorSelector;
import org.eclipse.aether.repository.ProxySelector;
import org.eclipse.aether.util.repository.AuthenticationBuilder;
import org.eclipse.aether.util.repository.ConservativeAuthenticationSelector;
import org.eclipse.aether.util.repository.DefaultAuthenticationSelector;
import org.eclipse.aether.util.repository.DefaultMirrorSelector;
import org.eclipse.aether.util.repository.DefaultProxySelector;
import org.sonatype.plexus.components.cipher.DefaultPlexusCipher;
import org.sonatype.plexus.components.cipher.PlexusCipherException;
import org.sonatype.plexus.components.sec.dispatcher.DefaultSecDispatcher;
import org.springframework.boot.cli.util.Log;
import org.springframework.boot.cli.compiler.MavenSettings;
import org.springframework.boot.cli.compiler.MavenSettingsReader;
/**
* Auto-configuration for a RepositorySystemSession that uses Maven's settings.xml to
......@@ -58,32 +31,16 @@ import org.springframework.boot.cli.util.Log;
public class SettingsXmlRepositorySystemSessionAutoConfiguration implements
RepositorySystemSessionAutoConfiguration {
private final String homeDir;
public SettingsXmlRepositorySystemSessionAutoConfiguration() {
this(System.getProperty("user.home"));
}
SettingsXmlRepositorySystemSessionAutoConfiguration(String homeDir) {
this.homeDir = homeDir;
}
@Override
public void apply(DefaultRepositorySystemSession session,
RepositorySystem repositorySystem) {
Settings settings = loadSettings();
SettingsDecryptionResult decryptionResult = decryptSettings(settings);
if (!decryptionResult.getProblems().isEmpty()) {
Log.error("Maven settings decryption failed. Some Maven repositories may be inaccessible");
// Continue - the encrypted credentials may not be used
}
MavenSettings settings = new MavenSettingsReader().readSettings();
session.setOffline(settings.isOffline());
session.setMirrorSelector(createMirrorSelector(settings));
session.setAuthenticationSelector(createAuthenticationSelector(decryptionResult
.getServers()));
session.setProxySelector(createProxySelector(decryptionResult.getProxies()));
session.setOffline(settings.getOffline());
session.setMirrorSelector(settings.getMirrorSelector());
session.setAuthenticationSelector(settings.getAuthenticationSelector());
session.setProxySelector(settings.getProxySelector());
String localRepository = settings.getLocalRepository();
if (localRepository != null) {
......@@ -92,92 +49,4 @@ public class SettingsXmlRepositorySystemSessionAutoConfiguration implements
}
}
private Settings loadSettings() {
File settingsFile = new File(this.homeDir, ".m2/settings.xml");
SettingsBuildingRequest request = new DefaultSettingsBuildingRequest();
request.setUserSettingsFile(settingsFile);
request.setSystemProperties(System.getProperties());
try {
return new DefaultSettingsBuilderFactory().newInstance().build(request)
.getEffectiveSettings();
}
catch (SettingsBuildingException ex) {
throw new IllegalStateException("Failed to build settings from "
+ settingsFile, ex);
}
}
private SettingsDecryptionResult decryptSettings(Settings settings) {
DefaultSettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest(
settings);
return createSettingsDecrypter().decrypt(request);
}
private SettingsDecrypter createSettingsDecrypter() {
SettingsDecrypter settingsDecrypter = new DefaultSettingsDecrypter();
setField(DefaultSettingsDecrypter.class, "securityDispatcher", settingsDecrypter,
new SpringBootSecDispatcher());
return settingsDecrypter;
}
private void setField(Class<?> clazz, String fieldName, Object target, Object value) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
catch (Exception e) {
throw new IllegalStateException("Failed to set field '" + fieldName
+ "' on '" + target + "'", e);
}
}
private MirrorSelector createMirrorSelector(Settings settings) {
DefaultMirrorSelector selector = new DefaultMirrorSelector();
for (Mirror mirror : settings.getMirrors()) {
selector.add(mirror.getId(), mirror.getUrl(), mirror.getLayout(), false,
mirror.getMirrorOf(), mirror.getMirrorOfLayouts());
}
return selector;
}
private AuthenticationSelector createAuthenticationSelector(List<Server> servers) {
DefaultAuthenticationSelector selector = new DefaultAuthenticationSelector();
for (Server server : servers) {
AuthenticationBuilder auth = new AuthenticationBuilder();
auth.addUsername(server.getUsername()).addPassword(server.getPassword());
auth.addPrivateKey(server.getPrivateKey(), server.getPassphrase());
selector.add(server.getId(), auth.build());
}
return new ConservativeAuthenticationSelector(selector);
}
private ProxySelector createProxySelector(List<Proxy> proxies) {
DefaultProxySelector selector = new DefaultProxySelector();
for (Proxy proxy : proxies) {
Authentication authentication = new AuthenticationBuilder()
.addUsername(proxy.getUsername()).addPassword(proxy.getPassword())
.build();
selector.add(new org.eclipse.aether.repository.Proxy(proxy.getProtocol(),
proxy.getHost(), proxy.getPort(), authentication), proxy
.getNonProxyHosts());
}
return selector;
}
private class SpringBootSecDispatcher extends DefaultSecDispatcher {
public SpringBootSecDispatcher() {
this._configurationFile = new File(
SettingsXmlRepositorySystemSessionAutoConfiguration.this.homeDir,
".m2/settings-security.xml").getAbsolutePath();
try {
this._cipher = new DefaultPlexusCipher();
}
catch (PlexusCipherException e) {
throw new IllegalStateException(e);
}
}
}
}
/*
* Copyright 2012-2015 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.compiler;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.junit.Test;
import org.springframework.boot.cli.compiler.grape.RepositoryConfiguration;
import org.springframework.boot.cli.util.SystemProperties;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link RepositoryConfigurationFactory}
*
* @author Andy Wilkinson
*/
public class RepositoryConfigurationFactoryTests {
@Test
public void defaultRepositories() {
SystemProperties.doWithSystemProperties(new Runnable() {
@Override
public void run() {
List<RepositoryConfiguration> repositoryConfiguration = RepositoryConfigurationFactory
.createDefaultRepositoryConfiguration();
assertRepositoryConfiguration(repositoryConfiguration, "central",
"local", "spring-snapshot", "spring-milestone");
}
}, "user.home:src/test/resources/maven-settings/basic");
}
@Test
public void snapshotRepositoriesDisabled() {
SystemProperties.doWithSystemProperties(
new Runnable() {
@Override
public void run() {
List<RepositoryConfiguration> repositoryConfiguration = RepositoryConfigurationFactory
.createDefaultRepositoryConfiguration();
assertRepositoryConfiguration(repositoryConfiguration, "central",
"local");
}
}, "user.home:src/test/resources/maven-settings/basic",
"disableSpringSnapshotRepos:true");
}
@Test
public void activeByDefaultProfileRepositories() {
SystemProperties.doWithSystemProperties(new Runnable() {
@Override
public void run() {
List<RepositoryConfiguration> repositoryConfiguration = RepositoryConfigurationFactory
.createDefaultRepositoryConfiguration();
assertRepositoryConfiguration(repositoryConfiguration, "central",
"local", "spring-snapshot", "spring-milestone",
"active-by-default");
}
}, "user.home:src/test/resources/maven-settings/active-profile-repositories");
}
@Test
public void activeByPropertyProfileRepositories() {
SystemProperties.doWithSystemProperties(
new Runnable() {
@Override
public void run() {
List<RepositoryConfiguration> repositoryConfiguration = RepositoryConfigurationFactory
.createDefaultRepositoryConfiguration();
assertRepositoryConfiguration(repositoryConfiguration, "central",
"local", "spring-snapshot", "spring-milestone",
"active-by-property");
}
},
"user.home:src/test/resources/maven-settings/active-profile-repositories",
"foo:bar");
}
private void assertRepositoryConfiguration(
List<RepositoryConfiguration> configurations, String... expectedNames) {
assertThat(configurations, hasSize(expectedNames.length));
Set<String> actualNames = new HashSet<String>();
for (RepositoryConfiguration configuration : configurations) {
actualNames.add(configuration.getName());
}
assertThat(actualNames, hasItems(expectedNames));
}
}
......@@ -37,6 +37,7 @@ import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.boot.cli.util.SystemProperties;
import static org.hamcrest.Matchers.endsWith;
import static org.junit.Assert.assertEquals;
......@@ -89,25 +90,33 @@ public class SettingsXmlRepositorySystemSessionAutoConfigurationTests {
}
});
System.setProperty("foo", "bar");
try {
new SettingsXmlRepositorySystemSessionAutoConfiguration(
"src/test/resources/maven-settings/property-interpolation").apply(
session, this.repositorySystem);
}
finally {
System.clearProperty("foo");
}
SystemProperties.doWithSystemProperties(
new Runnable() {
@Override
public void run() {
new SettingsXmlRepositorySystemSessionAutoConfiguration()
.apply(session,
SettingsXmlRepositorySystemSessionAutoConfigurationTests.this.repositorySystem);
}
}, "user.home:src/test/resources/maven-settings/property-interpolation",
"foo:bar");
assertThat(session.getLocalRepository().getBasedir().getAbsolutePath(),
endsWith(File.separatorChar + "bar" + File.separatorChar + "repository"));
}
private void assertSessionCustomization(String userHome) {
DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
final DefaultRepositorySystemSession session = MavenRepositorySystemUtils
.newSession();
new SettingsXmlRepositorySystemSessionAutoConfiguration(userHome).apply(session,
this.repositorySystem);
SystemProperties.doWithSystemProperties(new Runnable() {
@Override
public void run() {
new SettingsXmlRepositorySystemSessionAutoConfiguration()
.apply(session,
SettingsXmlRepositorySystemSessionAutoConfigurationTests.this.repositorySystem);
}
}, "user.home:" + userHome);
RemoteRepository repository = new RemoteRepository.Builder("my-server",
"default", "http://maven.example.com").build();
......@@ -151,4 +160,5 @@ public class SettingsXmlRepositorySystemSessionAutoConfigurationTests {
assertEquals("tester", authenticationContext.get(AuthenticationContext.USERNAME));
assertEquals("secret", authenticationContext.get(AuthenticationContext.PASSWORD));
}
}
/*
* Copyright 2012-2015 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.cli.util;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
/**
* Utilities for working with System properties in unit tests
*
* @author Andy Wilkinson
*/
public class SystemProperties {
/**
* Performs the given {@code action} with the given system properties set. System
* properties are restored to their previous values once the action has run.
*
* @param action The action to perform
* @param systemPropertyPairs The system properties, each in the form
* {@code key:value}
*/
public static void doWithSystemProperties(Runnable action,
String... systemPropertyPairs) {
Map<String, String> originalValues = new HashMap<String, String>();
for (String pair : systemPropertyPairs) {
String[] components = pair.split(":");
String key = components[0];
String value = components[1];
originalValues.put(key, System.setProperty(key, value));
}
try {
action.run();
}
finally {
for (Entry<String, String> entry : originalValues.entrySet()) {
if (entry.getValue() == null) {
System.clearProperty(entry.getKey());
}
else {
System.setProperty(entry.getKey(), entry.getValue());
}
}
}
}
}
......@@ -490,6 +490,24 @@ the top level, or you can put the beans DSL in a separate file if you prefer.
[[cli-maven-settings]]
== Configuring the CLI with settings.xml
The Spring Boot CLI uses Aether, Maven's dependency resolution engine, to resolve
dependencies. The CLI makes use of the Maven configuration found in `~/.m2/settings.xml`
to configure Aether. The following configuration settings are honored by the CLI:
* Offline
* Mirrors
* Servers
* Proxies
* Profiles
** Activation
** Repositories
* Active profiles
Please refer to https://maven.apache.org/settings.html[Maven's settings documentation] for
further information.
[[cli-whats-next]]
== What to read next
There are some {github-code}/spring-boot-cli/samples[sample groovy
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment