Browse Source

Make Profiles created via Profiles.of() comparable

Prior to this commit, a Profiles instance created via Profiles.of() was
not considered equivalent to another Profiles instance created via
Profiles.of() with the exact same expressions. This makes it difficult
to mock invocations of Environment#acceptsProfiles(Profiles) -- for
example, when using a mocking library such as Mockito.

This commit makes Profiles instances created via Profiles.of()
"comparable" by implementing equals() and hashCode() in ParsedProfiles.

Note, however, that equivalence is only guaranteed if the exact same
profile expression strings are supplied to Profiles.of(). In other
words, Profiles.of("A & B", "C | D") is equivalent to
Profiles.of("A & B", "C | D") and Profiles.of("C | D", "A & B"), but
Profiles.of("X & Y") is not equivalent to Profiles.of("X&Y") or
Profiles.of("Y & X").

Closes gh-25340
pull/25714/head
Sam Brannen 6 years ago
parent
commit
14d539017c
  1. 17
      spring-core/src/main/java/org/springframework/core/env/Profiles.java
  2. 34
      spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java
  3. 71
      spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java

17
spring-core/src/main/java/org/springframework/core/env/Profiles.java vendored

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -26,6 +26,7 @@ import java.util.function.Predicate;
* {@link #of(String...) of(...)} factory method. * {@link #of(String...) of(...)} factory method.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Sam Brannen
* @since 5.1 * @since 5.1
*/ */
@FunctionalInterface @FunctionalInterface
@ -34,7 +35,7 @@ public interface Profiles {
/** /**
* Test if this {@code Profiles} instance <em>matches</em> against the given * Test if this {@code Profiles} instance <em>matches</em> against the given
* active profiles predicate. * active profiles predicate.
* @param activeProfiles predicate that tests whether a given profile is * @param activeProfiles a predicate that tests whether a given profile is
* currently active * currently active
*/ */
boolean matches(Predicate<String> activeProfiles); boolean matches(Predicate<String> activeProfiles);
@ -49,16 +50,20 @@ public interface Profiles {
* {@code "production"}) or a profile expression. A profile expression allows * {@code "production"}) or a profile expression. A profile expression allows
* for more complicated profile logic to be expressed, for example * for more complicated profile logic to be expressed, for example
* {@code "production & cloud"}. * {@code "production & cloud"}.
* <p>The following operators are supported in profile expressions: * <p>The following operators are supported in profile expressions.
* <ul> * <ul>
* <li>{@code !} - A logical <em>not</em> of the profile</li> * <li>{@code !} - A logical <em>NOT</em> of the profile or profile expression</li>
* <li>{@code &} - A logical <em>and</em> of the profiles</li> * <li>{@code &} - A logical <em>AND</em> of the profiles or profile expressions</li>
* <li>{@code |} - A logical <em>or</em> of the profiles</li> * <li>{@code |} - A logical <em>OR</em> of the profiles or profile expressions</li>
* </ul> * </ul>
* <p>Please note that the {@code &} and {@code |} operators may not be mixed * <p>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 * without using parentheses. For example {@code "a & b | c"} is not a valid
* expression; it must be expressed as {@code "(a & b) | c"} or * expression; it must be expressed as {@code "(a & b) | c"} or
* {@code "a & (b | c)"}. * {@code "a & (b | c)"}.
* <p>As of Spring Framework 5.1.17, two {@code Profiles} instances returned
* by this method are considered equivalent to each other (in terms of
* {@code equals()} and {@code hashCode()} semantics) if they are created
* with identical <em>profile strings</em>.
* @param profiles the <em>profile strings</em> to include * @param profiles the <em>profile strings</em> to include
* @return a new {@link Profiles} instance * @return a new {@link Profiles} instance
*/ */

34
spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java vendored

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,7 +18,10 @@ package org.springframework.core.env;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -30,6 +33,7 @@ import org.springframework.util.StringUtils;
* Internal parser used by {@link Profiles#of}. * Internal parser used by {@link Profiles#of}.
* *
* @author Phillip Webb * @author Phillip Webb
* @author Sam Brannen
* @since 5.1 * @since 5.1
*/ */
final class ProfilesParser { final class ProfilesParser {
@ -56,6 +60,7 @@ final class ProfilesParser {
private static Profiles parseTokens(String expression, StringTokenizer tokens) { private static Profiles parseTokens(String expression, StringTokenizer tokens) {
return parseTokens(expression, tokens, Context.NONE); return parseTokens(expression, tokens, Context.NONE);
} }
private static Profiles parseTokens(String expression, StringTokenizer tokens, Context context) { private static Profiles parseTokens(String expression, StringTokenizer tokens, Context context) {
List<Profiles> elements = new ArrayList<>(); List<Profiles> elements = new ArrayList<>();
Operator operator = null; Operator operator = null;
@ -145,12 +150,12 @@ final class ProfilesParser {
private static class ParsedProfiles implements Profiles { private static class ParsedProfiles implements Profiles {
private final String[] expressions; private final Set<String> expressions = new LinkedHashSet<>();
private final Profiles[] parsed; private final Profiles[] parsed;
ParsedProfiles(String[] expressions, Profiles[] parsed) { ParsedProfiles(String[] expressions, Profiles[] parsed) {
this.expressions = expressions; Collections.addAll(this.expressions, expressions);
this.parsed = parsed; this.parsed = parsed;
} }
@ -164,10 +169,31 @@ final class ProfilesParser {
return false; return false;
} }
@Override
public int hashCode() {
return this.expressions.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ParsedProfiles that = (ParsedProfiles) obj;
return this.expressions.equals(that.expressions);
}
@Override @Override
public String toString() { public String toString() {
return StringUtils.arrayToDelimitedString(this.expressions, " or "); return StringUtils.collectionToDelimitedString(this.expressions, " or ");
} }
} }
} }

71
spring-core/src/test/java/org/springframework/core/env/ProfilesTests.java vendored

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -292,11 +292,72 @@ public class ProfilesTests {
@Test @Test
public void sensibleToString() { public void sensibleToString() {
assertEquals("spring & framework or java | kotlin", assertEquals("spring", Profiles.of("spring").toString());
Profiles.of("spring & framework", "java | kotlin").toString()); assertEquals("(spring & framework) | (spring & java)", Profiles.of("(spring & framework) | (spring & java)").toString());
assertEquals("(spring&framework)|(spring&java)", Profiles.of("(spring&framework)|(spring&java)").toString());
assertEquals("spring & framework or java | kotlin", Profiles.of("spring & framework", "java | kotlin").toString());
assertEquals("java | kotlin or spring & framework", Profiles.of("java | kotlin", "spring & framework").toString());
} }
private void assertMalformed(Supplier<Profiles> supplier) { @Test
public void sensibleEquals() {
assertEqual("(spring & framework) | (spring & java)");
assertEqual("(spring&framework)|(spring&java)");
assertEqual("spring & framework", "java | kotlin");
// Ensure order of individual expressions does not affect equals().
String expression1 = "A | B";
String expression2 = "C & (D | E)";
Profiles profiles1 = Profiles.of(expression1, expression2);
Profiles profiles2 = Profiles.of(expression2, expression1);
assertEquals(profiles1, profiles2);
assertEquals(profiles2, profiles1);
}
private void assertEqual(String... expressions) {
Profiles profiles1 = Profiles.of(expressions);
Profiles profiles2 = Profiles.of(expressions);
assertEquals(profiles1, profiles2);
assertEquals(profiles2, profiles1);
}
@Test
public void sensibleHashCode() {
assertHashCode("(spring & framework) | (spring & java)");
assertHashCode("(spring&framework)|(spring&java)");
assertHashCode("spring & framework", "java | kotlin");
// Ensure order of individual expressions does not affect hashCode().
String expression1 = "A | B";
String expression2 = "C & (D | E)";
Profiles profiles1 = Profiles.of(expression1, expression2);
Profiles profiles2 = Profiles.of(expression2, expression1);
assertEquals(profiles1.hashCode(), profiles2.hashCode());
}
private void assertHashCode(String... expressions) {
Profiles profiles1 = Profiles.of(expressions);
Profiles profiles2 = Profiles.of(expressions);
assertEquals(profiles1.hashCode(), profiles2.hashCode());
}
@Test
public void equalsAndHashCodeAreNotBasedOnLogicalStructureOfNodesWithinExpressionTree() {
Profiles profiles1 = Profiles.of("A | B");
Profiles profiles2 = Profiles.of("B | A");
assertTrue(profiles1.matches(activeProfiles("A")));
assertTrue(profiles1.matches(activeProfiles("B")));
assertTrue(profiles2.matches(activeProfiles("A")));
assertTrue(profiles2.matches(activeProfiles("B")));
assertNotEquals(profiles1, profiles2);
assertNotEquals(profiles2, profiles1);
assertNotEquals(profiles1.hashCode(), profiles2.hashCode());
}
private static void assertMalformed(Supplier<Profiles> supplier) {
try { try {
supplier.get(); supplier.get();
fail("Not malformed"); fail("Not malformed");
@ -305,7 +366,7 @@ public class ProfilesTests {
assertTrue(ex.getMessage().contains("Malformed")); assertTrue(ex.getMessage().contains("Malformed"));
} }
} }
private static Predicate<String> activeProfiles(String... profiles) { private static Predicate<String> activeProfiles(String... profiles) {
return new MockActiveProfiles(profiles); return new MockActiveProfiles(profiles);
} }

Loading…
Cancel
Save