Skip to content

Commit 2a3e4a1

Browse files
committed
Add Spring profiles expression support
1 parent 20d25d9 commit 2a3e4a1

File tree

14 files changed

+458
-34
lines changed

14 files changed

+458
-34
lines changed

ezkv-boot/src/main/java/io/jstach/ezkv/boot/EzkvConfig.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ record Event(System.Logger.Level level, String message) {
138138

139139
@Override
140140
public void init(KeyValuesSystem system) {
141-
events.add(new Event(System.Logger.Level.INFO, "Initializing"));
141+
events.add(new Event(System.Logger.Level.DEBUG, "Initializing"));
142142
}
143143

144144
@Override
@@ -159,7 +159,7 @@ public void warn(String message) {
159159

160160
@Override
161161
public void closed(KeyValuesSystem system) {
162-
System.Logger logger = System.getLogger("io.jstach.ezkv.boot.ezkvConfig");
162+
System.Logger logger = System.getLogger("io.jstach.ezkv.boot.EzkvConfig");
163163
Event e;
164164
while ((e = events.poll()) != null) {
165165
logger.log(e.level, e.message);
@@ -171,9 +171,9 @@ public void fatal(Exception exception) {
171171
var err = System.err;
172172
Event e;
173173
while ((e = events.poll()) != null) {
174-
err.println("[" + Logger.formatLevel(e.level) + "] io.jstach.ezkv.boot.ezkvConfig - " + e.message);
174+
err.println("[" + Logger.formatLevel(e.level) + "] io.jstach.ezkv.boot.EzkvConfig - " + e.message);
175175
}
176-
err.println("[ERROR] io.jstach.ezkv.boot.ezkvConfig - " + exception.getMessage());
176+
err.println("[ERROR] io.jstach.ezkv.boot.EzkvConfig - " + exception.getMessage());
177177
exception.printStackTrace(err);
178178
}
179179

@@ -201,6 +201,8 @@ public Logger getLogger() {
201201
try (var system = KeyValuesSystem.builder() //
202202
.useServiceLoader()
203203
.environment(new BootEnvironment(logger))
204+
.filter(new OnProfileKeyValuesFilter())
205+
.addPreFilter("onprofile", "")
204206
.build()) {
205207
var properties = Holder.PROPERTIES;
206208

@@ -209,7 +211,8 @@ public Logger getLogger() {
209211
if (properties != null) {
210212
loader.add("setDefaultProperties", properties);
211213
}
212-
var kvs = loader.variables(Variables::ofSystemProperties)
214+
var kvs = loader //
215+
.variables(Variables::ofSystemProperties)
213216
.variables(Variables::ofSystemEnv)
214217
.variables(RandomVariables::of)
215218
.add("classpath:/application.properties", b -> b.name("application").noRequire(true))
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.jstach.ezkv.boot;
2+
3+
import java.util.Optional;
4+
import java.util.Set;
5+
6+
import io.jstach.ezkv.kvs.KeyValues;
7+
import io.jstach.ezkv.kvs.KeyValuesException;
8+
import io.jstach.ezkv.kvs.KeyValuesServiceProvider.KeyValuesFilter;
9+
10+
class OnProfileKeyValuesFilter implements KeyValuesFilter {
11+
12+
@Override
13+
public Optional<KeyValues> filter(FilterContext context, KeyValues keyValues, Filter filter)
14+
throws IllegalArgumentException, KeyValuesException {
15+
if (!filter.filter().equals("onprofile")) {
16+
return Optional.empty();
17+
}
18+
var map = keyValues.toMap();
19+
String activateOn = context.environment().qualifyMetaKey("config.activate.on-profile");
20+
String profileExp = map.get(activateOn);
21+
if (profileExp == null) {
22+
return Optional.of(keyValues);
23+
}
24+
// context.environment().getLogger().debug("Found profile exp: " + profileExp);
25+
var _profiles = Profiles.of(profileExp);
26+
var selectedProfiles = Set.copyOf(context.profiles());
27+
if (_profiles.matches(selectedProfiles::contains)) {
28+
return Optional.of(keyValues.filter(kv -> !kv.key().equals(activateOn)));
29+
}
30+
return Optional.of(KeyValues.empty());
31+
}
32+
33+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.jstach.ezkv.boot;
18+
19+
import java.util.function.Predicate;
20+
21+
// TODO possibly move to ezkv-kvs
22+
/**
23+
* Profile predicate that may be {@linkplain Environment#acceptsProfiles(Profiles)
24+
* accepted} by an {@link Environment}.
25+
*
26+
* <p>
27+
* May be implemented directly or, more usually, created using the {@link #of(String...)
28+
* of(...)} factory method.
29+
*
30+
* @author Phillip Webb
31+
* @author Sam Brannen
32+
* @since 5.1
33+
* @see Environment#acceptsProfiles(Profiles)
34+
* @see Environment#matchesProfiles(String...)
35+
*/
36+
@FunctionalInterface
37+
interface Profiles {
38+
39+
/**
40+
* Test if this {@code Profiles} instance <em>matches</em> against the given
41+
* predicate.
42+
* @param isProfileActive a predicate that tests whether a given profile is currently
43+
* active
44+
*/
45+
boolean matches(Predicate<String> isProfileActive);
46+
47+
/**
48+
* Create a new {@link Profiles} instance that checks for matches against the given
49+
* <em>profile expressions</em>.
50+
* <p>
51+
* The returned instance will {@linkplain Profiles#matches(Predicate) match} if any
52+
* one of the given profile expressions matches.
53+
* <p>
54+
* A profile expression may contain a simple profile name (for example
55+
* {@code "production"}) or a compound expression. A compound expression allows for
56+
* more complicated profile logic to be expressed, for example
57+
* {@code "production & cloud"}.
58+
* <p>
59+
* The following operators are supported in profile expressions.
60+
* <ul>
61+
* <li>{@code !} - A logical <em>NOT</em> of the profile name or compound
62+
* expression</li>
63+
* <li>{@code &} - A logical <em>AND</em> of the profile names or compound
64+
* expressions</li>
65+
* <li>{@code |} - A logical <em>OR</em> of the profile names or compound
66+
* expressions</li>
67+
* </ul>
68+
* <p>
69+
* Please note that the {@code &} and {@code |} operators may not be mixed without
70+
* using parentheses. For example, {@code "a & b | c"} is not a valid expression: it
71+
* must be expressed as {@code "(a & b) | c"} or {@code "a & (b | c)"}.
72+
* <p>
73+
* Two {@code Profiles} instances returned by this method are considered equivalent to
74+
* each other (in terms of {@code equals()} and {@code hashCode()} semantics) if they
75+
* are created with identical <em>profile expressions</em>.
76+
* @param profileExpressions the <em>profile expressions</em> to include
77+
* @return a new {@link Profiles} instance
78+
*/
79+
static Profiles of(String... profileExpressions) {
80+
return ProfilesParser.parse(profileExpressions);
81+
}
82+
83+
}
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.jstach.ezkv.boot;
18+
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.Collections;
22+
import java.util.LinkedHashSet;
23+
import java.util.List;
24+
import java.util.Set;
25+
import java.util.StringTokenizer;
26+
import java.util.function.Predicate;
27+
import java.util.stream.Collectors;
28+
29+
import org.jspecify.annotations.Nullable;
30+
31+
/**
32+
* Internal parser used by {@link Profiles#of}.
33+
*
34+
* @author Phillip Webb
35+
* @author Sam Brannen
36+
* @since 5.1
37+
*/
38+
final class ProfilesParser {
39+
40+
private ProfilesParser() {
41+
}
42+
43+
static Profiles parse(String... expressions) {
44+
if (expressions.length == 0) {
45+
throw new IllegalArgumentException("Must specify at least one profile expression");
46+
}
47+
Profiles[] parsed = new Profiles[expressions.length];
48+
for (int i = 0; i < expressions.length; i++) {
49+
parsed[i] = parseExpression(expressions[i]);
50+
}
51+
return new ParsedProfiles(expressions, parsed);
52+
}
53+
54+
private static Profiles parseExpression(String expression) {
55+
if (expression.isBlank()) {
56+
throw new IllegalArgumentException("Invalid profile expression [" + expression + "]: must contain text");
57+
}
58+
StringTokenizer tokens = new StringTokenizer(expression, "()&|!", true);
59+
return parseTokens(expression, tokens);
60+
}
61+
62+
private static Profiles parseTokens(String expression, StringTokenizer tokens) {
63+
return parseTokens(expression, tokens, Context.NONE);
64+
}
65+
66+
private static Profiles parseTokens(String expression, StringTokenizer tokens, Context context) {
67+
List<Profiles> elements = new ArrayList<>();
68+
Operator operator = null;
69+
while (tokens.hasMoreTokens()) {
70+
String token = tokens.nextToken().trim();
71+
if (token.isEmpty()) {
72+
continue;
73+
}
74+
switch (token) {
75+
case "(" -> {
76+
Profiles contents = parseTokens(expression, tokens, Context.PARENTHESIS);
77+
if (context == Context.NEGATE) {
78+
return contents;
79+
}
80+
elements.add(contents);
81+
}
82+
case "&" -> {
83+
assertWellFormed(expression, operator == null || operator == Operator.AND);
84+
operator = Operator.AND;
85+
}
86+
case "|" -> {
87+
assertWellFormed(expression, operator == null || operator == Operator.OR);
88+
operator = Operator.OR;
89+
}
90+
case "!" -> elements.add(not(parseTokens(expression, tokens, Context.NEGATE)));
91+
case ")" -> {
92+
Profiles merged = merge(expression, elements, operator);
93+
if (context == Context.PARENTHESIS) {
94+
return merged;
95+
}
96+
elements.clear();
97+
elements.add(merged);
98+
operator = null;
99+
}
100+
default -> {
101+
Profiles value = equals(token);
102+
if (context == Context.NEGATE) {
103+
return value;
104+
}
105+
elements.add(value);
106+
}
107+
}
108+
}
109+
return merge(expression, elements, operator);
110+
}
111+
112+
private static Profiles merge(String expression, List<Profiles> elements, @Nullable Operator operator) {
113+
assertWellFormed(expression, !elements.isEmpty());
114+
if (elements.size() == 1) {
115+
return elements.get(0);
116+
}
117+
Profiles[] profiles = elements.toArray(new Profiles[0]);
118+
return (operator == Operator.AND ? and(profiles) : or(profiles));
119+
}
120+
121+
private static void assertWellFormed(String expression, boolean wellFormed) {
122+
if (!wellFormed) {
123+
throw new IllegalArgumentException("Malformed profile expression [" + expression + "]");
124+
}
125+
}
126+
127+
private static Profiles or(Profiles... profiles) {
128+
return activeProfile -> Arrays.stream(profiles).anyMatch(isMatch(activeProfile));
129+
}
130+
131+
private static Profiles and(Profiles... profiles) {
132+
return activeProfile -> Arrays.stream(profiles).allMatch(isMatch(activeProfile));
133+
}
134+
135+
private static Profiles not(Profiles profiles) {
136+
return activeProfile -> !profiles.matches(activeProfile);
137+
}
138+
139+
private static Profiles equals(String profile) {
140+
return activeProfile -> activeProfile.test(profile);
141+
}
142+
143+
private static Predicate<Profiles> isMatch(Predicate<String> activeProfiles) {
144+
return profiles -> profiles.matches(activeProfiles);
145+
}
146+
147+
private enum Operator {
148+
149+
AND, OR
150+
151+
}
152+
153+
private enum Context {
154+
155+
NONE, NEGATE, PARENTHESIS
156+
157+
}
158+
159+
private static class ParsedProfiles implements Profiles {
160+
161+
private final Set<String> expressions = new LinkedHashSet<>();
162+
163+
private final Profiles[] parsed;
164+
165+
ParsedProfiles(String[] expressions, Profiles[] parsed) {
166+
Collections.addAll(this.expressions, expressions);
167+
this.parsed = parsed;
168+
}
169+
170+
@Override
171+
public boolean matches(Predicate<String> activeProfiles) {
172+
for (Profiles candidate : this.parsed) {
173+
if (candidate.matches(activeProfiles)) {
174+
return true;
175+
}
176+
}
177+
return false;
178+
}
179+
180+
@Override
181+
public boolean equals(@Nullable Object other) {
182+
return (this == other
183+
|| (other instanceof ParsedProfiles that && this.expressions.equals(that.expressions)));
184+
}
185+
186+
@Override
187+
public int hashCode() {
188+
return this.expressions.hashCode();
189+
}
190+
191+
@Override
192+
public String toString() {
193+
if (this.expressions.size() == 1) {
194+
return this.expressions.iterator().next();
195+
}
196+
return this.expressions.stream().map(this::wrap).collect(Collectors.joining(" | "));
197+
}
198+
199+
private String wrap(String str) {
200+
return "(" + str + ")";
201+
}
202+
203+
}
204+
205+
}

ezkv-boot/src/main/java/io/jstach/ezkv/boot/RandomVariables.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public static RandomVariables of(KeyValuesEnvironment environment) {
3232
if (!key.startsWith(prefix)) {
3333
return null;
3434
}
35-
logger.debug(String.format("Generating random property for '%s'", key));
35+
if (logger.isDebug()) {
36+
logger.debug(String.format("Generating random property for '%s'", key));
37+
}
3638
return "" + getRandomValue(key.substring(prefix.length()));
3739
}
3840

0 commit comments

Comments
 (0)