From bd010494c9620afb3a165cc94990b68361847cf6 Mon Sep 17 00:00:00 2001 From: Matt Benson Date: Fri, 15 Jan 2016 16:38:12 -0600 Subject: [PATCH] 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())); + } + +}