From bd010494c9620afb3a165cc94990b68361847cf6 Mon Sep 17 00:00:00 2001 From: Matt Benson Date: Fri, 15 Jan 2016 16:38:12 -0600 Subject: [PATCH 1/2] Support profile negation in YAML sub-documents See gh-4953 --- .../main/asciidoc/spring-boot-features.adoc | 4 + .../yaml/SpringProfileDocumentMatcher.java | 66 ++++++++++- .../SpringProfileDocumentMatcherTests.java | 112 ++++++++++++++++++ 3 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 spring-boot/src/test/java/org/springframework/boot/yaml/SpringProfileDocumentMatcherTests.java 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..f736e9125c 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,10 @@ 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..bc8e4c57b5 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 @@ -18,23 +18,32 @@ package org.springframework.boot.yaml; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Properties; +import java.util.Set; 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 */ 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]; @@ -58,7 +67,58 @@ public class SpringProfileDocumentMatcher implements DocumentMatcher { if (profiles.length == 0) { profiles = DEFAULT_PROFILES; } - return new ArrayDocumentMatcher("spring.profiles", profiles).matches(properties); + ArrayDocumentMatcher next = new ArrayDocumentMatcher(SPRING_PROFILES, profiles); + + if (properties.containsKey(SPRING_PROFILES)) { + properties = new Properties(properties); + + Map sortedProfiles = sortProfiles( + properties.getProperty(SPRING_PROFILES)); + + // handle negated profiles: + if (sortedProfiles.containsKey(Boolean.FALSE)) { + properties.setProperty(SPRING_PROFILES, + sortedProfiles.get(Boolean.FALSE)); + + MatchStatus matchStatus = next.matches(properties); + switch (matchStatus) { + case FOUND: + return MatchStatus.NOT_FOUND; + case NOT_FOUND: + return MatchStatus.FOUND; + default: + break; + } + } + properties.setProperty(SPRING_PROFILES, sortedProfiles.get(Boolean.TRUE)); + } + return next.matches(properties); + } + + private Map sortProfiles(String value) { + if (value.indexOf('!') >= 0) { + Set positive = new HashSet(); + Set negative = new HashSet(); + for (String s : StringUtils.commaDelimitedListToSet(value)) { + if (s.charAt(0) == '!') { + negative.add(s.substring(1)); + } + else { + positive.add(s); + } + } + if (!negative.isEmpty()) { + Map result = new HashMap(); + result.put(Boolean.FALSE, + StringUtils.collectionToCommaDelimitedString(negative)); + if (!positive.isEmpty()) { + result.put(Boolean.TRUE, + StringUtils.collectionToCommaDelimitedString(positive)); + } + return result; + } + } + return Collections.singletonMap(Boolean.TRUE, value); } } 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..3cf31fd280 --- /dev/null +++ b/spring-boot/src/test/java/org/springframework/boot/yaml/SpringProfileDocumentMatcherTests.java @@ -0,0 +1,112 @@ +/* + * 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.Assert; +import org.junit.Test; + +import org.springframework.beans.factory.config.YamlProcessor.MatchStatus; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; + +/** + * Tests for {@link SpringProfileDocumentMatcher}. + * + * @author Matt Benson + */ +public class SpringProfileDocumentMatcherTests { + + @Test + public void testMatchesSingleProfile() throws IOException { + SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", + "bar"); + Assert.assertSame(MatchStatus.FOUND, + matcher.matches(getProperties("spring.profiles: foo"))); + } + + @Test + public void testAbstainNoConfiguredProfiles() throws IOException { + SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", + "bar"); + Assert.assertSame(MatchStatus.ABSTAIN, + matcher.matches(getProperties("some.property: spam"))); + } + + @Test + public void testNoActiveProfiles() throws IOException { + SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher(); + Assert.assertSame(MatchStatus.NOT_FOUND, + matcher.matches(getProperties("spring.profiles: bar,spam"))); + } + + @Test + public void testMatchesCommaSeparatedArray() throws IOException { + SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", + "bar"); + Assert.assertSame(MatchStatus.FOUND, + matcher.matches(getProperties("spring.profiles: bar,spam"))); + } + + @Test + public void testNoMatchingProfiles() throws IOException { + SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", + "bar"); + Assert.assertSame(MatchStatus.NOT_FOUND, + matcher.matches(getProperties("spring.profiles: baz,blah"))); + } + + @Test + public void testInverseMatchSingle() throws IOException { + SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", + "bar"); + Assert.assertSame(MatchStatus.FOUND, + matcher.matches(getProperties("spring.profiles: !baz"))); + } + + @Test + public void testInverseMatchMulti() throws IOException { + SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", + "bar"); + Assert.assertSame(MatchStatus.FOUND, + matcher.matches(getProperties("spring.profiles: !baz,!blah"))); + } + + @Test + public void testNegatedAndNonNegated() throws IOException { + SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", + "bar", "blah"); + Assert.assertSame(MatchStatus.FOUND, + matcher.matches(getProperties("spring.profiles: !baz,blah"))); + } + + @Test + public void testNegatedTrumpsMatching() throws IOException { + SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", + "baz", "blah"); + Assert.assertSame(MatchStatus.NOT_FOUND, + matcher.matches(getProperties("spring.profiles: !baz,blah"))); + } + + private Properties getProperties(String values) throws IOException { + return PropertiesLoaderUtils + .loadProperties(new ByteArrayResource(values.getBytes())); + } + +} From a39d351eedaf9b4a503d97afcea709e2aa8e7167 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Sun, 10 Apr 2016 23:21:23 -0700 Subject: [PATCH 2/2] Polish profile negation in YAML sub-documents Closes gh-4953 --- .../main/asciidoc/spring-boot-features.adoc | 9 +- .../yaml/SpringProfileDocumentMatcher.java | 92 ++++++++----------- .../SpringProfileDocumentMatcherTests.java | 86 ++++++++--------- 3 files changed, 85 insertions(+), 102 deletions(-) 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 f736e9125c..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,10 +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. +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 bc8e4c57b5..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. @@ -18,12 +18,8 @@ package org.springframework.boot.yaml; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashSet; -import java.util.Map; import java.util.Properties; -import java.util.Set; import org.springframework.beans.factory.config.YamlProcessor.DocumentMatcher; import org.springframework.beans.factory.config.YamlProcessor.MatchStatus; @@ -39,10 +35,12 @@ import org.springframework.util.StringUtils; * * @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]; @@ -63,62 +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; } - ArrayDocumentMatcher next = new ArrayDocumentMatcher(SPRING_PROFILES, profiles); - - if (properties.containsKey(SPRING_PROFILES)) { - properties = new Properties(properties); - - Map sortedProfiles = sortProfiles( - properties.getProperty(SPRING_PROFILES)); - - // handle negated profiles: - if (sortedProfiles.containsKey(Boolean.FALSE)) { - properties.setProperty(SPRING_PROFILES, - sortedProfiles.get(Boolean.FALSE)); - - MatchStatus matchStatus = next.matches(properties); - switch (matchStatus) { - case FOUND: - return MatchStatus.NOT_FOUND; - case NOT_FOUND: - return MatchStatus.FOUND; - default: - break; - } - } - properties.setProperty(SPRING_PROFILES, sortedProfiles.get(Boolean.TRUE)); - } - return next.matches(properties); + return new ArrayDocumentMatcher(SPRING_PROFILES, profiles); } - private Map sortProfiles(String value) { - if (value.indexOf('!') >= 0) { - Set positive = new HashSet(); - Set negative = new HashSet(); - for (String s : StringUtils.commaDelimitedListToSet(value)) { - if (s.charAt(0) == '!') { - negative.add(s.substring(1)); - } - else { - positive.add(s); - } + 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 (!negative.isEmpty()) { - Map result = new HashMap(); - result.put(Boolean.FALSE, - StringUtils.collectionToCommaDelimitedString(negative)); - if (!positive.isEmpty()) { - result.put(Boolean.TRUE, - StringUtils.collectionToCommaDelimitedString(positive)); - } - return result; + if (candidateType == type) { + result.append(result.length() > 0 ? "," : ""); + result.append(candidate.substring(type == ProfileType.POSITIVE ? 0 : 1)); } } - return Collections.singletonMap(Boolean.TRUE, value); + 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 index 3cf31fd280..b246b18957 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 @@ -19,13 +19,15 @@ package org.springframework.boot.yaml; import java.io.IOException; import java.util.Properties; -import org.junit.Assert; 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}. * @@ -34,79 +36,71 @@ import org.springframework.core.io.support.PropertiesLoaderUtils; public class SpringProfileDocumentMatcherTests { @Test - public void testMatchesSingleProfile() throws IOException { - SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", - "bar"); - Assert.assertSame(MatchStatus.FOUND, - matcher.matches(getProperties("spring.profiles: foo"))); + 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 testAbstainNoConfiguredProfiles() throws IOException { - SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", - "bar"); - Assert.assertSame(MatchStatus.ABSTAIN, - matcher.matches(getProperties("some.property: spam"))); + 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 testNoActiveProfiles() throws IOException { - SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher(); - Assert.assertSame(MatchStatus.NOT_FOUND, - matcher.matches(getProperties("spring.profiles: bar,spam"))); + 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 testMatchesCommaSeparatedArray() throws IOException { - SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", - "bar"); - Assert.assertSame(MatchStatus.FOUND, - matcher.matches(getProperties("spring.profiles: bar,spam"))); + 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 testNoMatchingProfiles() throws IOException { - SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", - "bar"); - Assert.assertSame(MatchStatus.NOT_FOUND, - matcher.matches(getProperties("spring.profiles: baz,blah"))); + 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 testInverseMatchSingle() throws IOException { - SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", - "bar"); - Assert.assertSame(MatchStatus.FOUND, - matcher.matches(getProperties("spring.profiles: !baz"))); + 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 { - SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", - "bar"); - Assert.assertSame(MatchStatus.FOUND, - matcher.matches(getProperties("spring.profiles: !baz,!blah"))); + DocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", "bar"); + Properties properties = getProperties("spring.profiles: !baz,!blah"); + assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.FOUND); } @Test - public void testNegatedAndNonNegated() throws IOException { - SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", - "bar", "blah"); - Assert.assertSame(MatchStatus.FOUND, - matcher.matches(getProperties("spring.profiles: !baz,blah"))); + 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 testNegatedTrumpsMatching() throws IOException { - SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher("foo", - "baz", "blah"); - Assert.assertSame(MatchStatus.NOT_FOUND, - matcher.matches(getProperties("spring.profiles: !baz,blah"))); + 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 { - return PropertiesLoaderUtils - .loadProperties(new ByteArrayResource(values.getBytes())); + ByteArrayResource resource = new ByteArrayResource(values.getBytes()); + return PropertiesLoaderUtils.loadProperties(resource); } }