diff --git a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc index d40b82bf82..1a1b7dd1a3 100644 --- a/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc +++ b/spring-boot-docs/src/main/asciidoc/spring-boot-features.adoc @@ -678,6 +678,11 @@ profile, and it would have to be explicitly reset in all other profiles as neces password: weak ---- +Spring profiles designated using the "spring.profiles" element may optionally be negated +using the {@code !} character. If both negated and non-negated profiles are specified for +a single document, at least one non-negated profile must match and no negated profiles +may match. + [[boot-features-external-config-yaml-shortcomings]] 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 dcd1dabafa..c57f9ae1af 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -24,18 +24,25 @@ import java.util.Properties; import org.springframework.beans.factory.config.YamlProcessor.DocumentMatcher; import org.springframework.beans.factory.config.YamlProcessor.MatchStatus; import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; /** * {@link DocumentMatcher} backed by {@link Environment#getActiveProfiles()}. A YAML - * document matches if it contains an element "spring.profiles" (a comma-separated list) - * and one of the profiles is in the active list. + * document may define a "spring.profiles" element as a comma-separated list of Spring + * profile names, optionally negated using the {@code !} character. If both negated and + * non-negated profiles are specified for a single document, at least one non-negated + * profile must match and no negated profiles may match. * * @author Dave Syer + * @author Matt Benson + * @author Phillip Webb */ public class SpringProfileDocumentMatcher implements DocumentMatcher { private static final String[] DEFAULT_PROFILES = new String[] { "^\\s*$" }; + private static final String SPRING_PROFILES = "spring.profiles"; + private String[] activeProfiles = new String[0]; public SpringProfileDocumentMatcher() { @@ -54,11 +61,52 @@ 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); + switch (activeProfilesMatcher.matches(properties)) { + case FOUND: + return MatchStatus.NOT_FOUND; + case NOT_FOUND: + return MatchStatus.FOUND; + } + properties.setProperty(SPRING_PROFILES, positive); + } + return activeProfilesMatcher.matches(properties); + } + + private DocumentMatcher getActiveProfilesDocumentMatcher() { String[] profiles = this.activeProfiles; if (profiles.length == 0) { profiles = DEFAULT_PROFILES; } - return new ArrayDocumentMatcher("spring.profiles", profiles).matches(properties); + return new ArrayDocumentMatcher(SPRING_PROFILES, profiles); + } + + private String extractProfiles(String profiles, ProfileType type) { + if (profiles == null) { + return null; + } + StringBuilder result = new StringBuilder(); + for (String candidate : StringUtils.commaDelimitedListToSet(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)); + } + } + return result.toString(); + } + + enum ProfileType { + POSITIVE, NEGATIVE } } 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 new file mode 100644 index 0000000000..b246b18957 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/yaml/SpringProfileDocumentMatcherTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2012-2016 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.yaml; + +import java.io.IOException; +import java.util.Properties; + +import org.junit.Test; + +import org.springframework.beans.factory.config.YamlProcessor.DocumentMatcher; +import org.springframework.beans.factory.config.YamlProcessor.MatchStatus; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SpringProfileDocumentMatcher}. + * + * @author Matt Benson + */ +public class SpringProfileDocumentMatcherTests { + + @Test + public void matchesSingleProfile() throws IOException { + DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar"); + Properties properties = getProperties("spring.profiles: foo"); + assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.FOUND); + } + + @Test + public void abstainNoConfiguredProfiles() throws IOException { + DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar"); + Properties properties = getProperties("some.property: spam"); + assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.ABSTAIN); + } + + @Test + public void noActiveProfiles() throws IOException { + DocumentMatcher matcher = new SpringProfileDocumentMatcher(); + Properties properties = getProperties("spring.profiles: bar,spam"); + assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.NOT_FOUND); + } + + @Test + public void matchesCommaSeparatedArray() 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 noMatchingProfiles() throws IOException { + DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar"); + Properties properties = getProperties("spring.profiles: baz,blah"); + assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.NOT_FOUND); + } + + @Test + public void inverseMatchSingle() throws IOException { + DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar"); + 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"); + assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.FOUND); + } + + @Test + public void negatedAndNonNegated() throws IOException { + DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar", "blah"); + Properties properties = getProperties("spring.profiles: !baz,blah"); + assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.FOUND); + } + + @Test + public void negatedTrumpsMatching() throws IOException { + DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "baz", "blah"); + Properties properties = getProperties("spring.profiles: !baz,blah"); + assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.NOT_FOUND); + } + + private Properties getProperties(String values) throws IOException { + ByteArrayResource resource = new ByteArrayResource(values.getBytes()); + return PropertiesLoaderUtils.loadProperties(resource); + } + +}