Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implicit filters to support things like spring.config.activate.on-profile #30

Merged
merged 3 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions ezkv-boot/src/main/java/io/jstach/ezkv/boot/EzkvConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ record Event(System.Logger.Level level, String message) {

@Override
public void init(KeyValuesSystem system) {
events.add(new Event(System.Logger.Level.INFO, "Initializing"));
events.add(new Event(System.Logger.Level.DEBUG, "Initializing"));
}

@Override
Expand All @@ -159,7 +159,7 @@ public void warn(String message) {

@Override
public void closed(KeyValuesSystem system) {
System.Logger logger = System.getLogger("io.jstach.ezkv.boot.ezkvConfig");
System.Logger logger = System.getLogger("io.jstach.ezkv.boot.EzkvConfig");
Event e;
while ((e = events.poll()) != null) {
logger.log(e.level, e.message);
Expand All @@ -171,9 +171,9 @@ public void fatal(Exception exception) {
var err = System.err;
Event e;
while ((e = events.poll()) != null) {
err.println("[" + Logger.formatLevel(e.level) + "] io.jstach.ezkv.boot.ezkvConfig - " + e.message);
err.println("[" + Logger.formatLevel(e.level) + "] io.jstach.ezkv.boot.EzkvConfig - " + e.message);
}
err.println("[ERROR] io.jstach.ezkv.boot.ezkvConfig - " + exception.getMessage());
err.println("[ERROR] io.jstach.ezkv.boot.EzkvConfig - " + exception.getMessage());
exception.printStackTrace(err);
}

Expand Down Expand Up @@ -201,6 +201,8 @@ public Logger getLogger() {
try (var system = KeyValuesSystem.builder() //
.useServiceLoader()
.environment(new BootEnvironment(logger))
.filter(new OnProfileKeyValuesFilter())
.addPreFilter("onprofile", "")
.build()) {
var properties = Holder.PROPERTIES;

Expand All @@ -209,7 +211,8 @@ public Logger getLogger() {
if (properties != null) {
loader.add("setDefaultProperties", properties);
}
var kvs = loader.variables(Variables::ofSystemProperties)
var kvs = loader //
.variables(Variables::ofSystemProperties)
.variables(Variables::ofSystemEnv)
.variables(RandomVariables::of)
.add("classpath:/application.properties", b -> b.name("application").noRequire(true))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.jstach.ezkv.boot;

import java.util.Optional;
import java.util.Set;

import io.jstach.ezkv.kvs.KeyValues;
import io.jstach.ezkv.kvs.KeyValuesException;
import io.jstach.ezkv.kvs.KeyValuesServiceProvider.KeyValuesFilter;

class OnProfileKeyValuesFilter implements KeyValuesFilter {

@Override
public Optional<KeyValues> filter(FilterContext context, KeyValues keyValues, Filter filter)
throws IllegalArgumentException, KeyValuesException {
if (!filter.filter().equals("onprofile")) {
return Optional.empty();
}
var map = keyValues.toMap();
String activateOn = context.environment().qualifyMetaKey("config.activate.on-profile");
String profileExp = map.get(activateOn);
if (profileExp == null) {
return Optional.of(keyValues);
}
// context.environment().getLogger().debug("Found profile exp: " + profileExp);
var _profiles = Profiles.of(profileExp);
var selectedProfiles = Set.copyOf(context.profiles());
if (_profiles.matches(selectedProfiles::contains)) {
return Optional.of(keyValues.filter(kv -> !kv.key().equals(activateOn)));
}
return Optional.of(KeyValues.empty());
}

}
83 changes: 83 additions & 0 deletions ezkv-boot/src/main/java/io/jstach/ezkv/boot/Profiles.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2002-2024 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
*
* https://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 io.jstach.ezkv.boot;

import java.util.function.Predicate;

// TODO possibly move to ezkv-kvs
/**
* Profile predicate that may be {@linkplain Environment#acceptsProfiles(Profiles)
* accepted} by an {@link Environment}.
*
* <p>
* May be implemented directly or, more usually, created using the {@link #of(String...)
* of(...)} factory method.
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.1
* @see Environment#acceptsProfiles(Profiles)
* @see Environment#matchesProfiles(String...)
*/
@FunctionalInterface
interface Profiles {

/**
* Test if this {@code Profiles} instance <em>matches</em> against the given
* predicate.
* @param isProfileActive a predicate that tests whether a given profile is currently
* active
*/
boolean matches(Predicate<String> isProfileActive);

/**
* Create a new {@link Profiles} instance that checks for matches against the given
* <em>profile expressions</em>.
* <p>
* The returned instance will {@linkplain Profiles#matches(Predicate) match} if any
* one of the given profile expressions matches.
* <p>
* A profile expression may contain a simple profile name (for example
* {@code "production"}) or a compound expression. A compound expression allows for
* more complicated profile logic to be expressed, for example
* {@code "production & cloud"}.
* <p>
* The following operators are supported in profile expressions.
* <ul>
* <li>{@code !} - A logical <em>NOT</em> of the profile name or compound
* expression</li>
* <li>{@code &} - A logical <em>AND</em> of the profile names or compound
* expressions</li>
* <li>{@code |} - A logical <em>OR</em> of the profile names or compound
* expressions</li>
* </ul>
* <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 expression: it
* must be expressed as {@code "(a & b) | c"} or {@code "a & (b | c)"}.
* <p>
* 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 expressions</em>.
* @param profileExpressions the <em>profile expressions</em> to include
* @return a new {@link Profiles} instance
*/
static Profiles of(String... profileExpressions) {
return ProfilesParser.parse(profileExpressions);
}

}
205 changes: 205 additions & 0 deletions ezkv-boot/src/main/java/io/jstach/ezkv/boot/ProfilesParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/*
* Copyright 2002-2023 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
*
* https://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 io.jstach.ezkv.boot;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.jspecify.annotations.Nullable;

/**
* Internal parser used by {@link Profiles#of}.
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.1
*/
final class ProfilesParser {

private ProfilesParser() {
}

static Profiles parse(String... expressions) {
if (expressions.length == 0) {
throw new IllegalArgumentException("Must specify at least one profile expression");
}
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) {
if (expression.isBlank()) {
throw new IllegalArgumentException("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) {
return parseTokens(expression, tokens, Context.NONE);
}

private static Profiles parseTokens(String expression, StringTokenizer tokens, Context context) {
List<Profiles> elements = new ArrayList<>();
Operator operator = null;
while (tokens.hasMoreTokens()) {
String token = tokens.nextToken().trim();
if (token.isEmpty()) {
continue;
}
switch (token) {
case "(" -> {
Profiles contents = parseTokens(expression, tokens, Context.PARENTHESIS);
if (context == Context.NEGATE) {
return contents;
}
elements.add(contents);
}
case "&" -> {
assertWellFormed(expression, operator == null || operator == Operator.AND);
operator = Operator.AND;
}
case "|" -> {
assertWellFormed(expression, operator == null || operator == Operator.OR);
operator = Operator.OR;
}
case "!" -> elements.add(not(parseTokens(expression, tokens, Context.NEGATE)));
case ")" -> {
Profiles merged = merge(expression, elements, operator);
if (context == Context.PARENTHESIS) {
return merged;
}
elements.clear();
elements.add(merged);
operator = null;
}
default -> {
Profiles value = equals(token);
if (context == Context.NEGATE) {
return value;
}
elements.add(value);
}
}
}
return merge(expression, elements, operator);
}

private static Profiles merge(String expression, List<Profiles> elements, @Nullable 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 ? and(profiles) : or(profiles));
}

private static void assertWellFormed(String expression, boolean wellFormed) {
if (!wellFormed) {
throw new IllegalArgumentException("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 activeProfile -> !profiles.matches(activeProfile);
}

private static Profiles equals(String profile) {
return activeProfile -> activeProfile.test(profile);
}

private static Predicate<Profiles> isMatch(Predicate<String> activeProfiles) {
return profiles -> profiles.matches(activeProfiles);
}

private enum Operator {

AND, OR

}

private enum Context {

NONE, NEGATE, PARENTHESIS

}

private static class ParsedProfiles implements Profiles {

private final Set<String> expressions = new LinkedHashSet<>();

private final Profiles[] parsed;

ParsedProfiles(String[] expressions, Profiles[] parsed) {
Collections.addAll(this.expressions, expressions);
this.parsed = parsed;
}

@Override
public boolean matches(Predicate<String> activeProfiles) {
for (Profiles candidate : this.parsed) {
if (candidate.matches(activeProfiles)) {
return true;
}
}
return false;
}

@Override
public boolean equals(@Nullable Object other) {
return (this == other
|| (other instanceof ParsedProfiles that && this.expressions.equals(that.expressions)));
}

@Override
public int hashCode() {
return this.expressions.hashCode();
}

@Override
public String toString() {
if (this.expressions.size() == 1) {
return this.expressions.iterator().next();
}
return this.expressions.stream().map(this::wrap).collect(Collectors.joining(" | "));
}

private String wrap(String str) {
return "(" + str + ")";
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ public static RandomVariables of(KeyValuesEnvironment environment) {
if (!key.startsWith(prefix)) {
return null;
}
logger.debug(String.format("Generating random property for '%s'", key));
if (logger.isDebug()) {
logger.debug(String.format("Generating random property for '%s'", key));
}
return "" + getRandomValue(key.substring(prefix.length()));
}

Expand Down
Loading
Loading