Skip to content

Commit 2c9e794

Browse files
committed
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
1 parent 8734c64 commit 2c9e794

File tree

3 files changed

+106
-13
lines changed

3 files changed

+106
-13
lines changed

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@
2626
* {@link #of(String...) of(...)} factory method.
2727
*
2828
* @author Phillip Webb
29+
* @author Sam Brannen
2930
* @since 5.1
3031
*/
3132
@FunctionalInterface
@@ -34,7 +35,7 @@ public interface Profiles {
3435
/**
3536
* Test if this {@code Profiles} instance <em>matches</em> against the given
3637
* active profiles predicate.
37-
* @param activeProfiles predicate that tests whether a given profile is
38+
* @param activeProfiles a predicate that tests whether a given profile is
3839
* currently active
3940
*/
4041
boolean matches(Predicate<String> activeProfiles);
@@ -49,16 +50,20 @@ public interface Profiles {
4950
* {@code "production"}) or a profile expression. A profile expression allows
5051
* for more complicated profile logic to be expressed, for example
5152
* {@code "production & cloud"}.
52-
* <p>The following operators are supported in profile expressions:
53+
* <p>The following operators are supported in profile expressions.
5354
* <ul>
54-
* <li>{@code !} - A logical <em>not</em> of the profile</li>
55-
* <li>{@code &} - A logical <em>and</em> of the profiles</li>
56-
* <li>{@code |} - A logical <em>or</em> of the profiles</li>
55+
* <li>{@code !} - A logical <em>NOT</em> of the profile or profile expression</li>
56+
* <li>{@code &} - A logical <em>AND</em> of the profiles or profile expressions</li>
57+
* <li>{@code |} - A logical <em>OR</em> of the profiles or profile expressions</li>
5758
* </ul>
5859
* <p>Please note that the {@code &} and {@code |} operators may not be mixed
5960
* without using parentheses. For example {@code "a & b | c"} is not a valid
6061
* expression; it must be expressed as {@code "(a & b) | c"} or
6162
* {@code "a & (b | c)"}.
63+
* <p>As of Spring Framework 5.1.17, two {@code Profiles} instances returned
64+
* by this method are considered equivalent to each other (in terms of
65+
* {@code equals()} and {@code hashCode()} semantics) if they are created
66+
* with identical <em>profile strings</em>.
6267
* @param profiles the <em>profile strings</em> to include
6368
* @return a new {@link Profiles} instance
6469
*/

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

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,7 +18,10 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.Arrays;
21+
import java.util.Collections;
22+
import java.util.LinkedHashSet;
2123
import java.util.List;
24+
import java.util.Set;
2225
import java.util.StringTokenizer;
2326
import java.util.function.Predicate;
2427

@@ -30,6 +33,7 @@
3033
* Internal parser used by {@link Profiles#of}.
3134
*
3235
* @author Phillip Webb
36+
* @author Sam Brannen
3337
* @since 5.1
3438
*/
3539
final class ProfilesParser {
@@ -56,6 +60,7 @@ private static Profiles parseExpression(String expression) {
5660
private static Profiles parseTokens(String expression, StringTokenizer tokens) {
5761
return parseTokens(expression, tokens, Context.NONE);
5862
}
63+
5964
private static Profiles parseTokens(String expression, StringTokenizer tokens, Context context) {
6065
List<Profiles> elements = new ArrayList<>();
6166
Operator operator = null;
@@ -145,12 +150,12 @@ private enum Context {NONE, INVERT, BRACKET}
145150

146151
private static class ParsedProfiles implements Profiles {
147152

148-
private final String[] expressions;
153+
private final Set<String> expressions = new LinkedHashSet<>();
149154

150155
private final Profiles[] parsed;
151156

152157
ParsedProfiles(String[] expressions, Profiles[] parsed) {
153-
this.expressions = expressions;
158+
Collections.addAll(this.expressions, expressions);
154159
this.parsed = parsed;
155160
}
156161

@@ -164,10 +169,31 @@ public boolean matches(Predicate<String> activeProfiles) {
164169
return false;
165170
}
166171

172+
@Override
173+
public int hashCode() {
174+
return this.expressions.hashCode();
175+
}
176+
177+
@Override
178+
public boolean equals(Object obj) {
179+
if (this == obj) {
180+
return true;
181+
}
182+
if (obj == null) {
183+
return false;
184+
}
185+
if (getClass() != obj.getClass()) {
186+
return false;
187+
}
188+
ParsedProfiles that = (ParsedProfiles) obj;
189+
return this.expressions.equals(that.expressions);
190+
}
191+
167192
@Override
168193
public String toString() {
169-
return StringUtils.arrayToDelimitedString(this.expressions, " or ");
194+
return StringUtils.collectionToDelimitedString(this.expressions, " or ");
170195
}
196+
171197
}
172198

173199
}

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

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -288,10 +288,72 @@ void malformedExpressions() {
288288

289289
@Test
290290
void sensibleToString() {
291-
assertThat(Profiles.of("spring & framework", "java | kotlin").toString()).isEqualTo("spring & framework or java | kotlin");
291+
assertThat(Profiles.of("spring")).hasToString("spring");
292+
assertThat(Profiles.of("(spring & framework) | (spring & java)")).hasToString("(spring & framework) | (spring & java)");
293+
assertThat(Profiles.of("(spring&framework)|(spring&java)")).hasToString("(spring&framework)|(spring&java)");
294+
assertThat(Profiles.of("spring & framework", "java | kotlin")).hasToString("spring & framework or java | kotlin");
295+
assertThat(Profiles.of("java | kotlin", "spring & framework")).hasToString("java | kotlin or spring & framework");
292296
}
293297

294-
private void assertMalformed(Supplier<Profiles> supplier) {
298+
@Test
299+
void sensibleEquals() {
300+
assertEqual("(spring & framework) | (spring & java)");
301+
assertEqual("(spring&framework)|(spring&java)");
302+
assertEqual("spring & framework", "java | kotlin");
303+
304+
// Ensure order of individual expressions does not affect equals().
305+
String expression1 = "A | B";
306+
String expression2 = "C & (D | E)";
307+
Profiles profiles1 = Profiles.of(expression1, expression2);
308+
Profiles profiles2 = Profiles.of(expression2, expression1);
309+
assertThat(profiles1).isEqualTo(profiles2);
310+
assertThat(profiles2).isEqualTo(profiles1);
311+
}
312+
313+
private void assertEqual(String... expressions) {
314+
Profiles profiles1 = Profiles.of(expressions);
315+
Profiles profiles2 = Profiles.of(expressions);
316+
assertThat(profiles1).isEqualTo(profiles2);
317+
assertThat(profiles2).isEqualTo(profiles1);
318+
}
319+
320+
@Test
321+
void sensibleHashCode() {
322+
assertHashCode("(spring & framework) | (spring & java)");
323+
assertHashCode("(spring&framework)|(spring&java)");
324+
assertHashCode("spring & framework", "java | kotlin");
325+
326+
// Ensure order of individual expressions does not affect hashCode().
327+
String expression1 = "A | B";
328+
String expression2 = "C & (D | E)";
329+
Profiles profiles1 = Profiles.of(expression1, expression2);
330+
Profiles profiles2 = Profiles.of(expression2, expression1);
331+
assertThat(profiles1).hasSameHashCodeAs(profiles2);
332+
}
333+
334+
private void assertHashCode(String... expressions) {
335+
Profiles profiles1 = Profiles.of(expressions);
336+
Profiles profiles2 = Profiles.of(expressions);
337+
assertThat(profiles1).hasSameHashCodeAs(profiles2);
338+
}
339+
340+
@Test
341+
void equalsAndHashCodeAreNotBasedOnLogicalStructureOfNodesWithinExpressionTree() {
342+
Profiles profiles1 = Profiles.of("A | B");
343+
Profiles profiles2 = Profiles.of("B | A");
344+
345+
assertThat(profiles1.matches(activeProfiles("A"))).isTrue();
346+
assertThat(profiles1.matches(activeProfiles("B"))).isTrue();
347+
assertThat(profiles2.matches(activeProfiles("A"))).isTrue();
348+
assertThat(profiles2.matches(activeProfiles("B"))).isTrue();
349+
350+
assertThat(profiles1).isNotEqualTo(profiles2);
351+
assertThat(profiles2).isNotEqualTo(profiles1);
352+
assertThat(profiles1.hashCode()).isNotEqualTo(profiles2.hashCode());
353+
}
354+
355+
356+
private static void assertMalformed(Supplier<Profiles> supplier) {
295357
assertThatIllegalArgumentException().isThrownBy(
296358
supplier::get)
297359
.withMessageContaining("Malformed");

0 commit comments

Comments
 (0)