From 3e9a4de8691b920ecd263b6380bdfafc021327e8 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 5 Dec 2016 12:11:24 +0000 Subject: [PATCH] Allow spring.profiles to be configured as a YAML list/array Closes gh-7397 --- .../yaml/SpringProfileDocumentMatcher.java | 118 +++++++++++------- .../SpringProfileDocumentMatcherTests.java | 36 ++++-- 2 files changed, 101 insertions(+), 53 deletions(-) diff --git a/spring-boot/src/main/java/org/springframework/boot/yaml/SpringProfileDocumentMatcher.java b/spring-boot/src/main/java/org/springframework/boot/yaml/SpringProfileDocumentMatcher.java index 8e0fec294e..63f4c6dab9 100644 --- a/spring-boot/src/main/java/org/springframework/boot/yaml/SpringProfileDocumentMatcher.java +++ b/spring-boot/src/main/java/org/springframework/boot/yaml/SpringProfileDocumentMatcher.java @@ -16,16 +16,25 @@ package org.springframework.boot.yaml; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.List; import java.util.Properties; import java.util.Set; +import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.config.YamlProcessor.DocumentMatcher; import org.springframework.beans.factory.config.YamlProcessor.MatchStatus; +import org.springframework.boot.bind.PropertySourcesPropertyValues; +import org.springframework.boot.bind.RelaxedDataBinder; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** @@ -38,11 +47,10 @@ import org.springframework.util.StringUtils; * @author Dave Syer * @author Matt Benson * @author Phillip Webb + * @author Andy Wilkinson */ public class SpringProfileDocumentMatcher implements DocumentMatcher { - private static final String SPRING_PROFILES = "spring.profiles"; - private String[] activeProfiles = new String[0]; public SpringProfileDocumentMatcher() { @@ -61,46 +69,54 @@ public class SpringProfileDocumentMatcher implements DocumentMatcher { @Override public MatchStatus matches(Properties properties) { - DocumentMatcher activeProfilesMatcher = getActiveProfilesDocumentMatcher(); - String profiles = properties.getProperty(SPRING_PROFILES); - String negative = extractProfiles(profiles, ProfileType.NEGATIVE); - String positive = extractProfiles(profiles, ProfileType.POSITIVE); - if (StringUtils.hasLength(negative)) { - properties = new Properties(properties); - properties.setProperty(SPRING_PROFILES, negative); - if (activeProfilesMatcher.matches(properties) == MatchStatus.FOUND) { + List profiles = extractSpringProfiles(properties); + ProfilesMatcher profilesMatcher = getProfilesMatcher(); + Set negative = extractProfiles(profiles, ProfileType.NEGATIVE); + Set positive = extractProfiles(profiles, ProfileType.POSITIVE); + if (!CollectionUtils.isEmpty(negative)) { + if (profilesMatcher.matches(negative) == MatchStatus.FOUND) { return MatchStatus.NOT_FOUND; } - if (StringUtils.isEmpty(positive)) { + if (CollectionUtils.isEmpty(positive)) { return MatchStatus.FOUND; } - properties.setProperty(SPRING_PROFILES, positive); } - return activeProfilesMatcher.matches(properties); + return profilesMatcher.matches(positive); + } + + private List extractSpringProfiles(Properties properties) { + SpringProperties springProperties = new SpringProperties(); + MutablePropertySources propertySources = new MutablePropertySources(); + propertySources.addFirst(new PropertiesPropertySource("profiles", properties)); + PropertyValues propertyValues = new PropertySourcesPropertyValues( + propertySources); + new RelaxedDataBinder(springProperties, "spring").bind(propertyValues); + List profiles = springProperties.getProfiles(); + return profiles; } - private DocumentMatcher getActiveProfilesDocumentMatcher() { - return this.activeProfiles.length == 0 ? new EmptyProfileDocumentMatcher() - : new ActiveProfilesDocumentMatcher( + private ProfilesMatcher getProfilesMatcher() { + return this.activeProfiles.length == 0 ? new EmptyProfilesMatcher() + : new ActiveProfilesMatcher( new HashSet(Arrays.asList(this.activeProfiles))); } - private String extractProfiles(String profiles, ProfileType type) { - if (profiles == null) { + private Set extractProfiles(List profiles, ProfileType type) { + if (CollectionUtils.isEmpty(profiles)) { return null; } - StringBuilder result = new StringBuilder(); - for (String candidate : StringUtils.commaDelimitedListToSet(profiles)) { + Set extractedProfiles = new HashSet(); + for (String candidate : profiles) { ProfileType candidateType = ProfileType.POSITIVE; if (candidate.startsWith("!")) { candidateType = ProfileType.NEGATIVE; } if (candidateType == type) { - result.append(result.length() > 0 ? "," : ""); - result.append(candidate.substring(type == ProfileType.POSITIVE ? 0 : 1)); + extractedProfiles.add(type == ProfileType.POSITIVE ? candidate + : candidate.substring(1)); } } - return result.toString(); + return extractedProfiles; } /** @@ -111,40 +127,35 @@ public class SpringProfileDocumentMatcher implements DocumentMatcher { } /** - * Base class for profile-based {@link DocumentMatcher DocumentMatchers}. + * Base class for profile matchers. */ - private static abstract class AbstractProfileDocumentMatcher - implements DocumentMatcher { + private static abstract class ProfilesMatcher { - @Override - public final MatchStatus matches(Properties properties) { - if (!properties.containsKey(SPRING_PROFILES)) { + public final MatchStatus matches(Set profiles) { + if (CollectionUtils.isEmpty(profiles)) { return MatchStatus.ABSTAIN; } - Set profiles = StringUtils - .commaDelimitedListToSet(properties.getProperty(SPRING_PROFILES)); - return matches(profiles); + return doMatches(profiles); } - protected abstract MatchStatus matches(Set profiles); + protected abstract MatchStatus doMatches(Set profiles); } /** - * {@link AbstractProfileDocumentMatcher} that matches a document when a value in - * {@code spring.profiles} is also in {@code spring.profiles.active}. + * {@link ProfileMatcher} that matches when a value in {@code spring.profiles} is also + * in {@code spring.profiles.active}. */ - private static class ActiveProfilesDocumentMatcher - extends AbstractProfileDocumentMatcher { + private static class ActiveProfilesMatcher extends ProfilesMatcher { private final Set activeProfiles; - ActiveProfilesDocumentMatcher(Set activeProfiles) { + ActiveProfilesMatcher(Set activeProfiles) { this.activeProfiles = activeProfiles; } @Override - protected MatchStatus matches(Set profiles) { + protected MatchStatus doMatches(Set profiles) { if (profiles.isEmpty()) { return MatchStatus.NOT_FOUND; } @@ -159,20 +170,19 @@ public class SpringProfileDocumentMatcher implements DocumentMatcher { } /** - * {@link AbstractProfileDocumentMatcher} that matches a document when {@code + * {@link ProfilesMatcher} that matches when {@code * spring.profiles} is empty or contains a value with no text. * * @see StringUtils#hasText(String) */ - private static class EmptyProfileDocumentMatcher - extends AbstractProfileDocumentMatcher { + private static class EmptyProfilesMatcher extends ProfilesMatcher { @Override - public MatchStatus matches(Set profiles) { - if (profiles.isEmpty()) { + public MatchStatus doMatches(Set springProfiles) { + if (springProfiles.isEmpty()) { return MatchStatus.FOUND; } - for (String profile : profiles) { + for (String profile : springProfiles) { if (!StringUtils.hasText(profile)) { return MatchStatus.FOUND; } @@ -182,4 +192,22 @@ public class SpringProfileDocumentMatcher implements DocumentMatcher { } + /** + * Class for binding {@code spring.profiles} property. + */ + @ConfigurationProperties("spring") + static class SpringProperties { + + private List profiles = new ArrayList(); + + public List getProfiles() { + return this.profiles; + } + + public void setProfiles(List profiles) { + this.profiles = profiles; + } + + } + } diff --git a/spring-boot/src/test/java/org/springframework/boot/yaml/SpringProfileDocumentMatcherTests.java b/spring-boot/src/test/java/org/springframework/boot/yaml/SpringProfileDocumentMatcherTests.java index c180d496e9..f5d5ebd295 100644 --- a/spring-boot/src/test/java/org/springframework/boot/yaml/SpringProfileDocumentMatcherTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/yaml/SpringProfileDocumentMatcherTests.java @@ -23,8 +23,8 @@ import org.junit.Test; import org.springframework.beans.factory.config.YamlProcessor.DocumentMatcher; import org.springframework.beans.factory.config.YamlProcessor.MatchStatus; +import org.springframework.beans.factory.config.YamlPropertiesFactoryBean; import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.support.PropertiesLoaderUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -32,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat; * Tests for {@link SpringProfileDocumentMatcher}. * * @author Matt Benson + * @author Andy Wilkinson */ public class SpringProfileDocumentMatcherTests { @@ -57,12 +58,28 @@ public class SpringProfileDocumentMatcherTests { } @Test - public void matchesCommaSeparatedArray() throws IOException { + public void matchesCommaSeparatedString() throws IOException { DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar"); Properties properties = getProperties("spring.profiles: bar,spam"); assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.FOUND); } + @Test + public void matchesCommaSeparatedArray() throws IOException { + DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar"); + Properties properties = getProperties( + String.format("spring.profiles: [bar, spam]")); + assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.FOUND); + } + + @Test + public void matchesList() throws IOException { + DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar"); + Properties properties = getProperties( + String.format("spring.profiles:%n - bar%n - spam")); + assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.FOUND); + } + @Test public void noMatchingProfiles() throws IOException { DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar"); @@ -73,41 +90,44 @@ public class SpringProfileDocumentMatcherTests { @Test public void inverseMatchSingle() throws IOException { DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar"); - Properties properties = getProperties("spring.profiles: !baz"); + Properties properties = getProperties("spring.profiles: '!baz'"); assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.FOUND); } @Test public void testInverseMatchMulti() throws IOException { DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar"); - Properties properties = getProperties("spring.profiles: !baz,!blah"); + Properties properties = getProperties("spring.profiles: '!baz,!blah'"); assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.FOUND); } @Test public void negatedWithMatch() throws Exception { DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar", "blah"); - Properties properties = getProperties("spring.profiles: !baz,blah"); + Properties properties = getProperties("spring.profiles: '!baz,blah'"); assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.FOUND); } @Test public void negatedWithNoMatch() throws IOException { DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar", "blah"); - Properties properties = getProperties("spring.profiles: !baz,another"); + Properties properties = getProperties("spring.profiles: '!baz,another'"); assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.NOT_FOUND); } @Test public void negatedTrumpsMatching() throws IOException { DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "baz", "blah"); - Properties properties = getProperties("spring.profiles: !baz,blah"); + Properties properties = getProperties("spring.profiles: '!baz,blah'"); assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.NOT_FOUND); } private Properties getProperties(String values) throws IOException { + YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean(); ByteArrayResource resource = new ByteArrayResource(values.getBytes()); - return PropertiesLoaderUtils.loadProperties(resource); + yamlPropertiesFactoryBean.setResources(resource); + yamlPropertiesFactoryBean.afterPropertiesSet(); + return yamlPropertiesFactoryBean.getObject(); } }