diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java
index b50b8b8242..94d82e2db7 100644
--- a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java
+++ b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2018 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.
@@ -323,19 +323,9 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
}
@Override
- public boolean acceptsProfiles(String... profiles) {
- Assert.notEmpty(profiles, "Must specify at least one profile");
- for (String profile : profiles) {
- if (StringUtils.hasLength(profile) && profile.charAt(0) == '!') {
- if (!isProfileActive(profile.substring(1))) {
- return true;
- }
- }
- else if (isProfileActive(profile)) {
- return true;
- }
- }
- return false;
+ public boolean acceptsProfiles(Profiles profiles) {
+ Assert.notNull(profiles, "Profiles must not be null");
+ return profiles.matches(this::isProfileActive);
}
/**
diff --git a/spring-core/src/main/java/org/springframework/core/env/Environment.java b/spring-core/src/main/java/org/springframework/core/env/Environment.java
index 52c8717e94..d335aa9db5 100644
--- a/spring-core/src/main/java/org/springframework/core/env/Environment.java
+++ b/spring-core/src/main/java/org/springframework/core/env/Environment.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2016 the original author or authors.
+ * Copyright 2002-2018 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.
@@ -97,15 +97,19 @@ public interface Environment extends PropertyResolver {
/**
* Return whether one or more of the given profiles is active or, in the case of no
* explicit active profiles, whether one or more of the given profiles is included in
- * the set of default profiles. If a profile begins with '!' the logic is inverted,
- * i.e. the method will return true if the given profile is not active.
- * For example,
env.acceptsProfiles("p1", "!p2") will
- * return {@code true} if profile 'p1' is active or 'p2' is not active.
- * @throws IllegalArgumentException if called with zero arguments
- * or if any profile is {@code null}, empty or whitespace-only
+ * the set of default profiles. Profiles can simple indicators ('{@code p1}',
+ * {@code !p1}) or more complex boolean expressions. See {@link Profiles#of(String...)}
+ * for syntax details.
* @see #getActiveProfiles
* @see #getDefaultProfiles
*/
- boolean acceptsProfiles(String... profiles);
+ default boolean acceptsProfiles(String... profiles) {
+ return acceptsProfiles(Profiles.of(profiles));
+ }
+
+ /**
+ * Returns whether the active profiles match the given {@link Profiles} set.
+ */
+ boolean acceptsProfiles(Profiles profiles);
}
diff --git a/spring-core/src/main/java/org/springframework/core/env/Profiles.java b/spring-core/src/main/java/org/springframework/core/env/Profiles.java
new file mode 100644
index 0000000000..dd09d7b504
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/core/env/Profiles.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2002-2018 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.core.env;
+
+/**
+ * A set of profiles that may be {@link Environment#acceptsProfiles(Profiles) accepted} by
+ * an {@link Environment}.
+ *
+ * May be implemented directly or, more usually, created using the {@link #of(String...)
+ * of(...)} factory method.
+ *
+ * @author Phillip Webb
+ * @since 5.0
+ * @see #of(String...)
+ */
+@FunctionalInterface
+public interface Profiles {
+
+ /**
+ * Test if this profile set matches against given active profiles.
+ */
+ boolean matches(ActiveProfiles activeProfiles);
+
+ /**
+ * Return a new {@link Profiles} instance that checks for matches against the given
+ * profile strings. The returned instance will
+ * {@link Profiles#matches(ActiveProfiles) matches} if any one of the given profile
+ * strings match.
+ *
+ * A profile string may contains a simple profile name (for example
+ * {@code "production"}) or a profile expression. A profile expression allows for more
+ * complicated profile logic to be expressed, for example
+ * {@code "production & cloud"}.
+ *
+ * The following operators are supported in profile expressions:
+ *
+ * - {@code !} - A logical not of the profile
+ * - {@code &} - A logical and of the profiles
+ * - {@code |} - A logical or of the profiles
+ *
+ * Please note that the {@code &} and {@code |} operators may not be mixed without
+ * using parentheses. For example {@code "a & b | c"} is not a valid expression, it
+ * must be expressed as {@code "(a & b) | c"}.
+ *
+ * @param profiles the profiles to include
+ * @return a new {@link Profiles} instance
+ */
+ public static Profiles of(String... profiles) {
+ return ProfilesParser.parse(profiles);
+ }
+
+ /**
+ * The current set of active profiles.
+ */
+ interface ActiveProfiles {
+
+ /**
+ * Tests if given profile is currently active.
+ */
+ boolean contains(String profile);
+ }
+}
diff --git a/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java
new file mode 100644
index 0000000000..bb1bd857f9
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2002-2018 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.core.env;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.StringTokenizer;
+import java.util.function.Predicate;
+
+import org.springframework.core.env.Profiles.ActiveProfiles;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Internal parser used by {@link Profiles#of}.
+ *
+ * @author Phillip Webb
+ * @since 5.0
+ */
+class ProfilesParser {
+
+ public static Profiles parse(String... expressions) {
+ Assert.notEmpty(expressions, "Must specify at least one profile");
+ Profiles[] parsed = new Profiles[expressions.length];
+ for (int i = 0; i < expressions.length; i++) {
+ parsed[i] = parseExpression(expressions[i]);
+ }
+ return new ParsedProfiles(expressions, parsed);
+ }
+
+ private static Profiles parseExpression(String expression) {
+ Assert.hasText(expression,
+ "Invalid profile expression [" + expression + "]: must contain text");
+ StringTokenizer tokens = new StringTokenizer(expression, "()&|!", true);
+ return parseTokens(expression, tokens);
+ }
+
+ private static Profiles parseTokens(String expression, StringTokenizer tokens) {
+ List elements = new ArrayList<>();
+ Operator operator = null;
+ while (tokens.hasMoreTokens()) {
+ String token = tokens.nextToken().trim();
+ if(token.isEmpty()) {
+ continue;
+ }
+ switch (token) {
+ case "(":
+ elements.add(parseTokens(expression, tokens));
+ break;
+ case "&":
+ assertWellFormed(expression, operator == null || operator == Operator.AND);
+ operator = Operator.AND;
+ break;
+ case "|":
+ assertWellFormed(expression, operator == null || operator == Operator.OR);
+ operator = Operator.OR;
+ break;
+ case "!":
+ elements.add(not(parseTokens(expression, tokens)));
+ break;
+ case ")":
+ Profiles merged = merge(expression, elements, operator);
+ elements.clear();
+ elements.add(merged);
+ operator = null;
+ break;
+ default:
+ elements.add(equals(token));
+ }
+ }
+ return merge(expression, elements, operator);
+ }
+
+ private static Profiles merge(String expression, List elements, Operator operator) {
+ assertWellFormed(expression, !elements.isEmpty());
+ if (elements.size() == 1) {
+ return elements.get(0);
+ }
+ Profiles[] profiles = elements.toArray(new Profiles[0]);
+ return (operator != Operator.AND ? or(profiles) : and(profiles));
+ }
+
+ private static void assertWellFormed(String expression, boolean wellFormed) {
+ Assert.isTrue(wellFormed,
+ () -> "Malformed profile expression '" + expression + "'");
+ }
+
+ private static Profiles or(Profiles... profiles) {
+ return (activeProfile) -> Arrays.stream(profiles).anyMatch(
+ isMatch(activeProfile));
+ }
+
+ private static Profiles and(Profiles... profiles) {
+ return (activeProfile) -> Arrays.stream(profiles).allMatch(
+ isMatch(activeProfile));
+ }
+
+ private static Profiles not(Profiles profiles) {
+ return (activeProfiles) -> !profiles.matches(activeProfiles);
+ }
+
+ private static Profiles equals(String profile) {
+ return (activeProfile) -> activeProfile.contains(profile);
+ }
+
+ private static Predicate isMatch(ActiveProfiles activeProfile) {
+ return (profiles) -> profiles.matches(activeProfile);
+ }
+
+ enum Operator {
+ AND, OR
+ };
+
+ private static class ParsedProfiles implements Profiles {
+
+ private final String[] expressions;
+
+ private final Profiles[] parsed;
+
+ public ParsedProfiles(String[] expressions, Profiles[] parsed) {
+ this.expressions = expressions;
+ this.parsed = parsed;
+ }
+
+ @Override
+ public boolean matches(ActiveProfiles activeProfiles) {
+ for (Profiles candidate : this.parsed) {
+ if (candidate.matches(activeProfiles)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return StringUtils.arrayToCommaDelimitedString(this.expressions);
+ }
+ }
+}
diff --git a/spring-core/src/test/java/org/springframework/core/env/DummyEnvironment.java b/spring-core/src/test/java/org/springframework/core/env/DummyEnvironment.java
index b74aa9ebc4..aa26747623 100644
--- a/spring-core/src/test/java/org/springframework/core/env/DummyEnvironment.java
+++ b/spring-core/src/test/java/org/springframework/core/env/DummyEnvironment.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2016 the original author or authors.
+ * Copyright 2002-2018 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.
@@ -74,8 +74,7 @@ public class DummyEnvironment implements Environment {
}
@Override
- public boolean acceptsProfiles(String... profiles) {
+ public boolean acceptsProfiles(Profiles profiles) {
return false;
}
-
}
diff --git a/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java b/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java
new file mode 100644
index 0000000000..13c7d659dc
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2002-2018 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.core.env;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.function.Supplier;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.springframework.core.env.Profiles.ActiveProfiles;
+import org.springframework.util.StringUtils;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link Profiles}.
+ *
+ * @author Phillip Webb
+ */
+public class ProfilesTests {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void ofWhenNullThrowsException() {
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Must specify at least one profile");
+ Profiles.of((String[]) null);
+ }
+
+ @Test
+ public void ofWhenEmptyThrowsException() {
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("Must specify at least one profile");
+ Profiles.of();
+ }
+
+ @Test
+ public void ofNullElement() throws Exception {
+ this.thrown.expect(IllegalArgumentException.class);
+ this.thrown.expectMessage("must contain text");
+ Profiles.of((String)null);
+ }
+
+ @Test
+ public void ofSingleElement() {
+ Profiles profiles = Profiles.of("spring");
+ assertTrue(profiles.matches(new MockActiveProfiles("spring")));
+ assertFalse(profiles.matches(new MockActiveProfiles("framework")));
+ }
+
+ @Test
+ public void ofSingleInvertedElement() {
+ Profiles profiles = Profiles.of("!spring");
+ assertFalse(profiles.matches(new MockActiveProfiles("spring")));
+ assertTrue(profiles.matches(new MockActiveProfiles("framework")));
+ }
+
+ @Test
+ public void ofMultipleElements() {
+ Profiles profiles = Profiles.of("spring", "framework");
+ assertTrue(profiles.matches(new MockActiveProfiles("spring")));
+ assertTrue(profiles.matches(new MockActiveProfiles("framework")));
+ assertFalse(profiles.matches(new MockActiveProfiles("java")));
+ }
+
+ @Test
+ public void ofMultipleElementsWithInverted() {
+ Profiles profiles = Profiles.of("!spring", "framework");
+ assertFalse(profiles.matches(new MockActiveProfiles("spring")));
+ assertTrue(profiles.matches(new MockActiveProfiles("framework")));
+ assertTrue(profiles.matches(new MockActiveProfiles("spring", "framework")));
+ }
+
+ @Test
+ public void ofMultipleElementsAllInverted() {
+ Profiles profiles = Profiles.of("!spring", "!framework");
+ assertTrue(profiles.matches(new MockActiveProfiles("spring")));
+ assertTrue(profiles.matches(new MockActiveProfiles("framework")));
+ assertFalse(profiles.matches(new MockActiveProfiles("spring", "framework")));
+ assertFalse(
+ profiles.matches(new MockActiveProfiles("spring", "framework", "java")));
+ }
+
+ @Test
+ public void ofSingleExpression() throws Exception {
+ Profiles profiles = Profiles.of("(spring)");
+ assertTrue(profiles.matches(new MockActiveProfiles("spring")));
+ assertFalse(profiles.matches(new MockActiveProfiles("framework")));
+ }
+
+ @Test
+ public void ofSingleInvertedExpression() throws Exception {
+ Profiles profiles = Profiles.of("(!spring)");
+ assertFalse(profiles.matches(new MockActiveProfiles("spring")));
+ assertTrue(profiles.matches(new MockActiveProfiles("framework")));
+ }
+
+ @Test
+ public void ofOrExpression() throws Exception {
+ Profiles profiles = Profiles.of("(spring | framework)");
+ assertTrue(profiles.matches(new MockActiveProfiles("spring")));
+ assertTrue(profiles.matches(new MockActiveProfiles("framework")));
+ assertTrue(profiles.matches(new MockActiveProfiles("spring", "framework")));
+ assertFalse(profiles.matches(new MockActiveProfiles("java")));
+ }
+
+ @Test
+ public void ofAndExpression() throws Exception {
+ Profiles profiles = Profiles.of("(spring & framework)");
+ assertFalse(profiles.matches(new MockActiveProfiles("spring")));
+ assertFalse(profiles.matches(new MockActiveProfiles("framework")));
+ assertTrue(profiles.matches(new MockActiveProfiles("spring", "framework")));
+ assertFalse(profiles.matches(new MockActiveProfiles("java")));
+ }
+
+ @Test
+ public void ofAndExpressionWithoutBraces() throws Exception {
+ Profiles profiles = Profiles.of("spring & framework");
+ assertFalse(profiles.matches(new MockActiveProfiles("spring")));
+ assertFalse(profiles.matches(new MockActiveProfiles("framework")));
+ assertTrue(profiles.matches(new MockActiveProfiles("spring", "framework")));
+ assertFalse(profiles.matches(new MockActiveProfiles("java")));
+ }
+
+ @Test
+ public void ofNotAndExpression() throws Exception {
+ Profiles profiles = Profiles.of("!(spring & framework)");
+ assertTrue(profiles.matches(new MockActiveProfiles("spring")));
+ assertTrue(profiles.matches(new MockActiveProfiles("framework")));
+ assertFalse(profiles.matches(new MockActiveProfiles("spring", "framework")));
+ assertTrue(profiles.matches(new MockActiveProfiles("java")));
+ }
+
+ @Test
+ public void ofNotOrExpression() throws Exception {
+ Profiles profiles = Profiles.of("!(spring | framework)");
+ assertFalse(profiles.matches(new MockActiveProfiles("spring")));
+ assertFalse(profiles.matches(new MockActiveProfiles("framework")));
+ assertFalse(profiles.matches(new MockActiveProfiles("spring", "framework")));
+ assertTrue(profiles.matches(new MockActiveProfiles("java")));
+ }
+
+ @Test
+ public void ofComplex() throws Exception {
+ Profiles profiles = Profiles.of("(spring & framework) | (spring & java)");
+ assertFalse(profiles.matches(new MockActiveProfiles("spring")));
+ assertTrue(profiles.matches(new MockActiveProfiles("spring", "framework")));
+ assertTrue(profiles.matches(new MockActiveProfiles("spring", "java")));
+ assertFalse(profiles.matches(new MockActiveProfiles("java", "framework")));
+ }
+
+ @Test
+ public void malformedExpressions() throws Exception {
+ assertMalformed(() -> Profiles.of("("));
+ assertMalformed(() -> Profiles.of(")"));
+ assertMalformed(() -> Profiles.of("a & b | c"));
+ }
+
+ private void assertMalformed(Supplier supplier) {
+ try {
+ supplier.get();
+ fail("Not malformed");
+ }
+ catch (IllegalArgumentException ex) {
+ assertTrue(ex.getMessage().contains("Malformed"));
+ }
+ }
+
+ private static class MockActiveProfiles implements ActiveProfiles {
+
+ private Set activeProfiles;
+
+ private Set defaultProfiles;
+
+
+ public MockActiveProfiles(String... activeProfiles) {
+ this(Arrays.asList(activeProfiles));
+ }
+
+ public MockActiveProfiles(Collection activeProfiles) {
+ this(activeProfiles, Collections.singleton("default"));
+ }
+
+ public MockActiveProfiles(Collection activeProfiles,
+ Collection defaultProfiles) {
+ this.activeProfiles = new LinkedHashSet<>(activeProfiles);
+ this.defaultProfiles = new LinkedHashSet<>(defaultProfiles);
+ }
+
+
+ @Override
+ public boolean contains(String profile) {
+ if (!StringUtils.hasText(profile) || profile.charAt(0) == '!') {
+ throw new IllegalArgumentException("Invalid profile [" + profile + "]");
+ }
+ return (this.activeProfiles.contains(profile)
+ || (this.activeProfiles.isEmpty() && this.defaultProfiles.contains(profile)));
+ }
+ }
+
+}
diff --git a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java
index 0ffb1fd132..0f0266b8f0 100644
--- a/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java
+++ b/spring-core/src/test/java/org/springframework/core/env/StandardEnvironmentTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2018 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.
@@ -283,7 +283,6 @@ public class StandardEnvironmentTests {
environment.acceptsProfiles("");
}
-
@Test
public void acceptsProfiles_activeProfileSetProgrammatically() {
assertThat(environment.acceptsProfiles("p1", "p2"), is(false));
@@ -326,6 +325,15 @@ public class StandardEnvironmentTests {
environment.acceptsProfiles("p1", "!");
}
+ @Test
+ public void acceptsProfiles_withProfileExpression() throws Exception {
+ assertThat(environment.acceptsProfiles("p1 & p2"), is(false));
+ environment.addActiveProfile("p1");
+ assertThat(environment.acceptsProfiles("p1 & p2"), is(false));
+ environment.addActiveProfile("p2");
+ assertThat(environment.acceptsProfiles("p1 & p2"), is(true));
+ }
+
@Test
public void environmentSubclass_withCustomProfileValidation() {
ConfigurableEnvironment env = new AbstractEnvironment() {