Conditionally enable and disable CLI options (#25333)

* Conditionally enable and disable CLI options

Closes #13113

Signed-off-by: Martin Bartoš <mabartos@redhat.com>

* Support for duplicates in config

Signed-off-by: Martin Bartoš <mabartos@redhat.com>

* Fix rendering config options in docs

Fixes #26515

Signed-off-by: Martin Bartoš <mabartos@redhat.com>

* Reorder OptionsDistTest

Signed-off-by: Martin Bartoš <mabartos@redhat.com>

---------

Signed-off-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Martin Bartoš 2024-03-07 21:36:43 +01:00 committed by GitHub
parent 40385061f7
commit e4aa1b5f95
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 1250 additions and 489 deletions

View file

@ -215,7 +215,7 @@ public class Profile {
private static final Logger logger = Logger.getLogger(Profile.class);
private static Profile CURRENT;
private static volatile Profile CURRENT;
private final ProfileName profileName;

View file

@ -1,6 +1,6 @@
<#import "/templates/options.adoc" as opts>
<#macro guide title summary priority=999 includedOptions="" preview="" tileVisible="true" previewDiscussionLink="">
<#macro guide title summary priority=999 deniedCategories="" includedOptions="" preview="" tileVisible="true" previewDiscussionLink="">
:guide-id: ${id}
:guide-title: ${title}
:guide-summary: ${summary}
@ -28,6 +28,6 @@ endif::[]
<#if includedOptions?has_content>
== Relevant options
<@opts.list options=ctx.options.getOptions(includedOptions) anchor=false></@opts.list>
<@opts.list options=ctx.options.getOptions(includedOptions, deniedCategories) anchor=false></@opts.list>
</#if>
</#macro>

View file

@ -23,6 +23,10 @@
*Env:* `${option.keyEnv}`
--
<#if option.enabledWhen?has_content>
${option.enabledWhen!}
</#if>
<#if option.deprecated?has_content>
<#-- Either mark the whole option as deprecated, or just selected values -->
<#if !option.deprecated.deprecatedValues?has_content>

View file

@ -15,40 +15,54 @@ import org.keycloak.provider.Spi;
import org.keycloak.quarkus.runtime.Providers;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import org.keycloak.utils.StringUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
public class Options {
private final Map<String, Option> options;
private final Map<OptionCategory, Set<Option>> options;
private final Map<String, Map<String, List<Option>>> providerOptions = new LinkedHashMap<>();
@SuppressWarnings("unchecked")
public Options() {
options = PropertyMappers.getMappers().stream()
this.options = new EnumMap<>(OptionCategory.class);
PropertyMappers.getMappers().stream()
.filter(m -> !m.isHidden())
.filter(propertyMapper -> Objects.nonNull(propertyMapper.getDescription()))
.map(m -> new Option(m.getFrom(), m.getCategory(), m.isBuildTime(), null, m.getDescription(), (String) m.getDefaultValue().map(Object::toString).orElse(null), m.getExpectedValues(), (DeprecatedMetadata) m.getDeprecatedMetadata().orElse(null)))
.sorted(Comparator.comparing(Option::getKey))
.collect(Collectors.toMap(Option::getKey, o -> o, (o1, o2) -> o1, LinkedHashMap::new)); // Need to ignore duplicate keys??
.map(m -> new Option(m.getFrom(),
m.getCategory(),
m.isBuildTime(),
null,
m.getDescription(),
m.getDefaultValue().map(Object::toString).orElse(null),
m.getExpectedValues(),
m.getEnabledWhen().orElse(""),
m.getDeprecatedMetadata().orElse(null)))
.forEach(o -> options.computeIfAbsent(o.category, k -> new TreeSet<>(Comparator.comparing(Option::getKey))).add(o));
ProviderManager providerManager = Providers.getProviderManager(Thread.currentThread().getContextClassLoader());
options.forEach((s, option) -> {
option.description = option.description.replaceAll("'([^ ]*)'", "`$1`");
});
options.values()
.stream()
.flatMap(Collection::stream)
.forEach(option -> option.description = option.description.replaceAll("'([^ ]*)'", "`$1`"));
for (Spi loadSpi : providerManager.loadSpis().stream().sorted(Comparator.comparing(Spi::getName)).collect(Collectors.toList())) {
for (ProviderFactory providerFactory : providerManager.load(loadSpi).stream().sorted(Comparator.comparing(ProviderFactory::getId)).collect(Collectors.toList())) {
for (Spi loadSpi : providerManager.loadSpis().stream().sorted(Comparator.comparing(Spi::getName)).toList()) {
for (ProviderFactory<?> providerFactory : providerManager.load(loadSpi).stream().sorted(Comparator.comparing(ProviderFactory::getId)).toList()) {
List<ProviderConfigProperty> configMetadata = providerFactory.getConfigMetadata();
if (configMetadata == null) {
@ -62,6 +76,7 @@ public class Options {
m.getHelpText(),
m.getDefaultValue() == null ? null : m.getDefaultValue().toString(),
m.getOptions() == null ? Collections.emptyList() : m.getOptions(),
"",
null))
.sorted(Comparator.comparing(Option::getKey)).collect(Collectors.toList());
@ -88,39 +103,84 @@ public class Options {
.collect(Collectors.toList());
}
public Collection<Option> getValues() {
return options.values();
}
public Collection<Option> getValues(OptionCategory category) {
return options.values().stream().filter(o -> o.category.equals(category)).collect(Collectors.toList());
return options.getOrDefault(category, Collections.emptySet());
}
public Option getOption(String key) {
return options.get(key);
Set<Option> foundOptions = options.values().stream().flatMap(Collection::stream).filter(f -> f.getKey().equals(key)).collect(Collectors.toSet());
if (foundOptions.size() > 1) {
final var categories = foundOptions.stream().map(f -> f.category).map(OptionCategory::getHeading).collect(Collectors.joining(","));
throw new IllegalArgumentException(String.format("Ambiguous options '%s' with categories: %s\n", key, categories));
}
return foundOptions.iterator().next();
}
public List<Option> getOptions(String includeOptions) {
/**
* Get denied categories for guide options
* <p>
* Used in cases when multiple options can be found under the same name
* By providing 'deniedCategories' parameter, we will not search for the option in these categories
* <p>
* f.e. when we specify {@code includedOptions="hostname"}, we should provide also {@code deniedCategories="hostname_v2"}
* In that case, we will use the option from the old hostname provider
*
* @return denied categories, otherwise an empty set
*/
public Set<OptionCategory> getDeniedCategories(String deniedCategories) {
return Optional.ofNullable(deniedCategories)
.filter(StringUtil::isNotBlank)
.map(f -> f.split(" "))
.map(Arrays::asList)
.map(f -> f.stream()
.map(g -> {
try {
return OptionCategory.valueOf(g.toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("You have specified wrong category name in the 'deniedCategories' property", e);
}
})
.collect(Collectors.toSet()))
.orElseGet(Collections::emptySet);
}
public List<Option> getOptions(String includeOptions, String deniedCategories) {
final String r = includeOptions.replaceAll("\\.", "\\\\.").replaceAll("\\*", ".*").replace(' ', '|');
return this.options.values().stream().filter(o -> o.getKey().matches(r)).collect(Collectors.toList());
final Set<OptionCategory> denied = getDeniedCategories(deniedCategories);
return options.values()
.stream()
.flatMap(Collection::stream)
.filter(f -> !denied.contains(f.category))
.filter(f -> f.getKey().matches(r))
.toList();
}
public Map<String, Map<String, List<Option>>> getProviderOptions() {
return providerOptions;
}
public class Option {
public static class Option {
private String key;
private OptionCategory category;
private boolean build;
private String type;
private final String key;
private final OptionCategory category;
private final boolean build;
private final String type;
private String description;
private String defaultValue;
private final String defaultValue;
private List<String> expectedValues;
private DeprecatedMetadata deprecated;
private final String enabledWhen;
private final DeprecatedMetadata deprecated;
public Option(String key, OptionCategory category, boolean build, String type, String description, String defaultValue, Iterable<String> expectedValues, DeprecatedMetadata deprecatedMetadata) {
public Option(String key,
OptionCategory category,
boolean build,
String type,
String description,
String defaultValue,
Iterable<String> expectedValues,
String enabledWhen,
DeprecatedMetadata deprecatedMetadata) {
this.key = key;
this.category = category;
this.build = build;
@ -128,6 +188,7 @@ public class Options {
this.description = description;
this.defaultValue = defaultValue;
this.expectedValues = StreamSupport.stream(expectedValues.spliterator(), false).collect(Collectors.toList());
this.enabledWhen = enabledWhen;
this.deprecated = deprecatedMetadata;
}
@ -178,6 +239,11 @@ public class Options {
return expectedValues;
}
public String getEnabledWhen() {
if (StringUtil.isBlank(enabledWhen)) return null;
return enabledWhen;
}
public DeprecatedMetadata getDeprecated() {
return deprecated;
}

View file

@ -44,14 +44,10 @@ public enum OptionCategory {
}
private String getHeadingBySupportLevel(String heading) {
if (this.supportLevel.equals(ConfigSupportLevel.EXPERIMENTAL)){
heading = heading + " (Experimental)";
}
if (this.supportLevel.equals(ConfigSupportLevel.PREVIEW)){
heading = heading + " (Preview)";
}
return heading;
return switch (supportLevel) {
case EXPERIMENTAL -> heading + " (Experimental)";
case PREVIEW -> heading + " (Preview)";
default -> heading;
};
}
}

View file

@ -68,6 +68,8 @@ import org.keycloak.common.Profile;
import org.keycloak.common.crypto.FipsMode;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.config.DatabaseOptions;
import org.keycloak.config.HealthOptions;
import org.keycloak.config.MetricsOptions;
import org.keycloak.config.SecurityOptions;
import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
import org.keycloak.connections.jpa.JpaConnectionProvider;
@ -915,11 +917,11 @@ class KeycloakProcessor {
}
private boolean isMetricsEnabled() {
return Configuration.getOptionalBooleanValue(NS_KEYCLOAK_PREFIX.concat("metrics-enabled")).orElse(false);
return Configuration.isTrue(MetricsOptions.METRICS_ENABLED);
}
private boolean isHealthEnabled() {
return Configuration.getOptionalBooleanValue(NS_KEYCLOAK_PREFIX.concat("health-enabled")).orElse(false);
return Configuration.isTrue(HealthOptions.HEALTH_ENABLED);
}
static JdbcDataSourceBuildItem getDefaultDataSource(List<JdbcDataSourceBuildItem> jdbcDataSources) {

View file

@ -38,6 +38,7 @@ import org.apache.commons.lang3.SystemUtils;
import org.keycloak.common.Profile;
import org.keycloak.common.profile.PropertiesFileProfileConfigResolver;
import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
public final class Environment {
@ -50,6 +51,9 @@ public final class Environment {
public static final String DEV_PROFILE_VALUE = "dev";
public static final String PROD_PROFILE_VALUE = "prod";
public static final String LAUNCH_MODE = "kc.launch.mode";
private static volatile AbstractCommand parsedCommand;
private Environment() {}
public static Boolean isRebuild() {
@ -255,4 +259,19 @@ public final class Environment {
return profile;
}
/**
* Get parsed AbstractCommand we obtained from the CLI
*/
public static Optional<AbstractCommand> getParsedCommand() {
return Optional.ofNullable(parsedCommand);
}
public static boolean isParsedCommand(String commandName) {
return getParsedCommand().filter(f -> f.getName().equals(commandName)).isPresent();
}
public static void setParsedCommand(AbstractCommand command) {
Environment.parsedCommand = command;
}
}

View file

@ -31,6 +31,8 @@ import java.util.ArrayList;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
import org.keycloak.common.profile.ProfileException;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import picocli.CommandLine.ExitCode;
import io.quarkus.runtime.ApplicationLifecycleManager;
@ -68,10 +70,7 @@ public class KeycloakMain implements QuarkusApplication {
try {
cliArgs = Picocli.parseArgs(args);
} catch (PropertyException e) {
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
PrintWriter errStream = new PrintWriter(System.err, true);
errorHandler.error(errStream, e.getMessage(), null);
System.exit(ExitCode.USAGE);
handleUsageError(e.getMessage());
return;
}
@ -79,25 +78,24 @@ public class KeycloakMain implements QuarkusApplication {
cliArgs = new ArrayList<>(cliArgs);
// default to show help message
cliArgs.add("-h");
} else if (isFastStart(cliArgs)) {
// fast path for starting the server without bootstrapping CLI
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
PrintWriter errStream = new PrintWriter(System.err, true);
} else if (isFastStart(cliArgs)) { // fast path for starting the server without bootstrapping CLI
if (isDevProfileNotAllowed()) {
errorHandler.error(errStream, Messages.devProfileNotAllowedError(Start.NAME), null);
System.exit(ExitCode.USAGE);
handleUsageError(Messages.devProfileNotAllowedError(Start.NAME));
return;
}
try {
PropertyMappers.sanitizeDisabledMappers();
Picocli.validateConfig(cliArgs, new Start());
} catch (PropertyException e) {
errorHandler.error(errStream, e.getMessage(), null);
System.exit(ExitCode.USAGE);
} catch (PropertyException | ProfileException e) {
handleUsageError(e.getMessage(), e.getCause());
return;
}
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
PrintWriter errStream = new PrintWriter(System.err, true);
start(errorHandler, errStream, args);
return;
@ -107,6 +105,17 @@ public class KeycloakMain implements QuarkusApplication {
parseAndRun(cliArgs);
}
private static void handleUsageError(String message) {
handleUsageError(message, null);
}
private static void handleUsageError(String message, Throwable cause) {
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
PrintWriter errStream = new PrintWriter(System.err, true);
errorHandler.error(errStream, message, cause);
System.exit(ExitCode.USAGE);
}
private static boolean isFastStart(List<String> cliArgs) {
// 'start --optimized' should start the server without parsing CLI
return cliArgs.size() == 2 && cliArgs.get(0).equals(Start.NAME) && cliArgs.stream().anyMatch(OPTIMIZED_BUILD_OPTION_LONG::equals);

View file

@ -161,8 +161,21 @@ public final class Help extends CommandLine.Help {
PropertyMapper<?> mapper = getMapper(option.longestName());
if (mapper == null) {
final var disabledMapper = PropertyMappers.getDisabledMapper(option.longestName());
final var isDisabledMapper = disabledMapper.isPresent();
// Show disabled mappers, which do not have a description when they're enabled
final var isEnabledWhenEmpty = isDisabledMapper && disabledMapper.get().getEnabledWhen().isEmpty();
if (isEnabledWhenEmpty) {
return true;
}
if (allOptions && isDisabledMapper) {
return true;
}
// only filter mapped options, defaults to the hidden marker
return !option.hidden();
return !option.hidden() && !isDisabledMapper;
}
boolean isUnsupportedOption = !PropertyMappers.isSupported(mapper);

View file

@ -17,8 +17,10 @@
package org.keycloak.quarkus.runtime.cli;
import static java.lang.String.format;
import static java.util.Optional.ofNullable;
import static java.util.stream.StreamSupport.stream;
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
import static org.keycloak.quarkus.runtime.Environment.isRebuildCheck;
import static org.keycloak.quarkus.runtime.Environment.isRebuilt;
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG;
@ -40,7 +42,6 @@ import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
@ -56,11 +57,13 @@ import java.util.stream.Collectors;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.jboss.logging.Logger;
import org.keycloak.common.profile.ProfileException;
import org.keycloak.config.DeprecatedMetadata;
import org.keycloak.config.Option;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.cli.command.HelpAllMixin;
import org.keycloak.quarkus.runtime.cli.command.ImportRealmMixin;
import org.keycloak.quarkus.runtime.cli.command.Main;
import org.keycloak.quarkus.runtime.cli.command.ShowConfig;
@ -68,6 +71,8 @@ import org.keycloak.quarkus.runtime.cli.command.StartDev;
import org.keycloak.quarkus.runtime.cli.command.Tools;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor;
import org.keycloak.quarkus.runtime.configuration.KcUnmatchedArgumentException;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor;
import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource;
@ -94,6 +99,7 @@ public final class Picocli {
private static class IncludeOptions {
boolean includeRuntime;
boolean includeBuildTime;
boolean includeDisabled;
}
private Picocli() {
@ -101,31 +107,45 @@ public final class Picocli {
public static void parseAndRun(List<String> cliArgs) {
CommandLine cmd = createCommandLine(cliArgs);
String[] argArray = cliArgs.toArray(new String[0]);
if (Environment.isRebuildCheck()) {
int exitCode = 0;
try {
// process the cli args first to init the config file and perform validation
cmd.parseArgs(argArray);
exitCode = runReAugmentationIfNeeded(cliArgs, cmd);
} catch (ParameterException ex) {
try {
exitCode = cmd.getParameterExceptionHandler().handleParseException(ex, argArray);
} catch (Exception e) {
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
errorHandler.error(cmd.getErr(), e.getMessage(), null);
exitCode = ex.getCommandLine().getCommandSpec().exitCodeOnInvalidInput();
}
}
exitOnFailure(exitCode, cmd);
return;
}
int exitCode = cmd.execute(argArray);
try {
cmd.parseArgs(argArray); // process the cli args first to init the config file and perform validation
int exitCode;
if (isRebuildCheck()) {
exitCode = runReAugmentationIfNeeded(cliArgs, cmd);
} else {
PropertyMappers.sanitizeDisabledMappers();
exitCode = cmd.execute(argArray);
}
exitOnFailure(exitCode, cmd);
} catch (ParameterException parEx) {
catchParameterException(parEx, cmd, argArray);
} catch (ProfileException | PropertyException proEx) {
catchProfileException(proEx.getMessage(), proEx.getCause(), cmd);
}
}
private static void catchParameterException(ParameterException parEx, CommandLine cmd, String[] args) {
int exitCode;
try {
exitCode = cmd.getParameterExceptionHandler().handleParseException(parEx, args);
} catch (Exception e) {
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
errorHandler.error(cmd.getErr(), e.getMessage(), null);
exitCode = parEx.getCommandLine().getCommandSpec().exitCodeOnInvalidInput();
}
exitOnFailure(exitCode, cmd);
}
private static void catchProfileException(String message, Throwable cause, CommandLine cmd) {
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
errorHandler.error(cmd.getErr(), message, cause);
exitOnFailure(CommandLine.ExitCode.USAGE, cmd);
}
private static void exitOnFailure(int exitCode, CommandLine cmd) {
if (exitCode != cmd.getCommandSpec().exitCodeOnSuccess() && !Environment.isTestLaunchMode() || isRebuildCheck()) {
// hard exit wanted, as build failed and no subsequent command should be executed. no quarkus involved.
@ -157,6 +177,7 @@ public final class Picocli {
}
}
if (requiresReAugmentation(currentCommandSpec)) {
PropertyMappers.sanitizeDisabledMappers();
exitCode = runReAugmentation(cliArgs, cmd);
}
@ -279,13 +300,27 @@ public final class Picocli {
return;
}
final boolean disabledMappersInterceptorEnabled = DisabledMappersInterceptor.isEnabled(); // return to the state before the disable
try {
PropertyMappingInterceptor.disable(); // we don't want the mapped / transformed properties, we want what the user effectively supplied
List<String> ignoredBuildTime = new ArrayList<>();
List<String> ignoredRunTime = new ArrayList<>();
Set<String> deprecatedInUse = new HashSet<>();
DisabledMappersInterceptor.disable(); // we want all properties, even disabled ones
final List<String> ignoredBuildTime = new ArrayList<>();
final List<String> ignoredRunTime = new ArrayList<>();
final Set<String> disabledBuildTime = new HashSet<>();
final Set<String> disabledRunTime = new HashSet<>();
final Set<String> deprecatedInUse = new HashSet<>();
final Set<PropertyMapper<?>> disabledMappers = new HashSet<>();
if (options.includeBuildTime) {
disabledMappers.addAll(PropertyMappers.getDisabledBuildTimeMappers().values());
}
if (options.includeRuntime) {
disabledMappers.addAll(PropertyMappers.getDisabledRuntimeMappers().values());
}
for (OptionCategory category : abstractCommand.getOptionCategories()) {
List<PropertyMapper<?>> mappers = new ArrayList<>();
List<PropertyMapper<?>> mappers = new ArrayList<>(disabledMappers);
Optional.ofNullable(PropertyMappers.getRuntimeMappers().get(category)).ifPresent(mappers::addAll);
Optional.ofNullable(PropertyMappers.getBuildTimeMappers().get(category)).ifPresent(mappers::addAll);
for (PropertyMapper<?> mapper : mappers) {
@ -297,6 +332,20 @@ public final class Picocli {
continue;
}
if (disabledMappers.contains(mapper)) {
if (!PropertyMappers.isDisabledMapper(mapper.getFrom())) {
continue; // we found enabled mapper with the same name
}
final boolean deniedPrintException = mapper.isRunTime() && isRebuild();
if (PropertyMapper.isCliOption(configValue) && !deniedPrintException) {
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine(), List.of(mapper.getCliFormat()));
} else {
handleDisabled(mapper.isRunTime() ? disabledRunTime : disabledBuildTime, mapper);
}
continue;
}
if (mapper.isBuildTime() && !options.includeBuildTime) {
ignoredBuildTime.add(mapper.getFrom());
continue;
@ -322,10 +371,17 @@ public final class Picocli {
outputIgnoredProperties(ignoredRunTime, false, logger);
}
if (!disabledBuildTime.isEmpty()) {
outputDisabledProperties(disabledBuildTime, true, logger);
} else if (!disabledRunTime.isEmpty()) {
outputDisabledProperties(disabledRunTime, false, logger);
}
if (!deprecatedInUse.isEmpty()) {
logger.warn("The following used options or option values are DEPRECATED and will be removed in a future release:\n" + String.join("\n", deprecatedInUse) + "\nConsult the Release Notes for details.");
}
} finally {
DisabledMappersInterceptor.enable(disabledMappersInterceptorEnabled);
PropertyMappingInterceptor.enable();
}
}
@ -372,10 +428,36 @@ public final class Picocli {
deprecatedInUse.add(sb.toString());
}
private static void handleDisabled(Set<String> disabledInUse, PropertyMapper<?> mapper) {
String optionName = mapper.getFrom();
if (optionName.startsWith(NS_KEYCLOAK_PREFIX)) {
optionName = optionName.substring(NS_KEYCLOAK_PREFIX.length());
}
final StringBuilder sb = new StringBuilder("\t- ");
sb.append(optionName);
if (mapper.getEnabledWhen().isPresent()) {
final String enabledWhen = mapper.getEnabledWhen().get();
sb.append(": ");
sb.append(enabledWhen);
if (!enabledWhen.endsWith(".")) {
sb.append(".");
}
}
disabledInUse.add(sb.toString());
}
private static void outputIgnoredProperties(List<String> properties, boolean build, Logger logger) {
logger.warn(String.format("The following %s time non-cli options were found, but will be ignored during %s time: %s\n",
logger.warn(format("The following %s time non-cli options were found, but will be ignored during %s time: %s\n",
build ? "build" : "run", build ? "run" : "build",
properties.stream().collect(Collectors.joining(", "))));
String.join(", ", properties)));
}
private static void outputDisabledProperties(Set<String> properties, boolean build, Logger logger) {
logger.warn(format("The following used %s time options are UNAVAILABLE and will be ignored during %s time:\n %s",
build ? "build" : "run", build ? "run" : "build",
String.join("\n", properties)));
}
private static boolean hasConfigChanges(CommandLine cmdCommand) {
@ -536,6 +618,7 @@ public final class Picocli {
}
result.includeRuntime = abstractCommand.includeRuntime();
result.includeBuildTime = abstractCommand.includeBuildTime();
result.includeDisabled = cliArgs.contains(HelpAllMixin.HELP_ALL_OPTION);
if (!result.includeBuildTime && !result.includeRuntime) {
return result;
@ -548,14 +631,17 @@ public final class Picocli {
}
private static void addCommandOptions(List<String> cliArgs, CommandLine command) {
if (command != null && command.getCommand() instanceof AbstractCommand) {
if (command != null && command.getCommand() instanceof AbstractCommand ac) {
IncludeOptions options = getIncludeOptions(cliArgs, command.getCommand(), command.getCommandName());
// set current parsed command
Environment.setParsedCommand(ac);
if (!options.includeBuildTime && !options.includeRuntime) {
return;
}
addOptionsToCli(command, options.includeBuildTime, options.includeRuntime);
addOptionsToCli(command, options);
}
}
@ -571,30 +657,48 @@ public final class Picocli {
return null;
}
private static void addOptionsToCli(CommandLine commandLine, boolean includeBuildTime, boolean includeRuntime) {
Map<OptionCategory, List<PropertyMapper<?>>> mappers = new EnumMap<>(OptionCategory.class);
private static void addOptionsToCli(CommandLine commandLine, IncludeOptions includeOptions) {
final Map<OptionCategory, List<PropertyMapper<?>>> mappers = new EnumMap<>(OptionCategory.class);
if (includeRuntime) {
if (includeOptions.includeRuntime) {
mappers.putAll(PropertyMappers.getRuntimeMappers());
if (includeOptions.includeDisabled) {
appendDisabledMappers(mappers, PropertyMappers.getDisabledRuntimeMappers());
}
}
if (includeBuildTime) {
for (Map.Entry<OptionCategory, List<PropertyMapper<?>>> entry : PropertyMappers.getBuildTimeMappers()
.entrySet()) {
List<PropertyMapper<?>> result = new ArrayList<>(mappers.getOrDefault(entry.getKey(), Collections.emptyList()));
if (includeOptions.includeBuildTime) {
combinePropertyMappers(mappers, PropertyMappers.getBuildTimeMappers());
result.addAll(entry.getValue());
mappers.put(entry.getKey(), result);
if (includeOptions.includeDisabled) {
appendDisabledMappers(mappers, PropertyMappers.getDisabledBuildTimeMappers());
}
}
addMappedOptionsToArgGroups(commandLine, mappers);
}
private static void appendDisabledMappers(Map<OptionCategory, List<PropertyMapper<?>>> origMappers,
Map<String, PropertyMapper<?>> additionalMappers) {
for (var pm : additionalMappers.values()) {
final List<PropertyMapper<?>> result = origMappers.getOrDefault(pm.getCategory(), new ArrayList<>());
result.add(pm);
origMappers.put(pm.getCategory(), result);
}
}
private static <T extends Map<OptionCategory, List<PropertyMapper<?>>>> void combinePropertyMappers(T origMappers, T additionalMappers) {
for (var entry : additionalMappers.entrySet()) {
final List<PropertyMapper<?>> result = origMappers.getOrDefault(entry.getKey(), new ArrayList<>());
result.addAll(entry.getValue());
origMappers.put(entry.getKey(), result);
}
}
private static void addMappedOptionsToArgGroups(CommandLine commandLine, Map<OptionCategory, List<PropertyMapper<?>>> propertyMappers) {
CommandSpec cSpec = commandLine.getCommandSpec();
for(OptionCategory category : ((AbstractCommand) commandLine.getCommand()).getOptionCategories()) {
for (OptionCategory category : ((AbstractCommand) commandLine.getCommand()).getOptionCategories()) {
List<PropertyMapper<?>> mappersInCategory = propertyMappers.get(category);
if (mappersInCategory == null) {
@ -607,11 +711,13 @@ public final class Picocli {
.order(category.getOrder())
.validate(false);
for (PropertyMapper<?> mapper: mappersInCategory) {
final Set<String> alreadyPresentArgs = new HashSet<>();
for (PropertyMapper<?> mapper : mappersInCategory) {
String name = mapper.getCliFormat();
String description = mapper.getDescription();
if (description == null || cSpec.optionsMap().containsKey(name) || name.endsWith(OPTION_PART_SEPARATOR)) {
if (description == null || cSpec.optionsMap().containsKey(name) || name.endsWith(OPTION_PART_SEPARATOR) || alreadyPresentArgs.contains(name)) {
//when key is already added or has no description, don't add.
continue;
}
@ -638,6 +744,8 @@ public final class Picocli {
optBuilder.type(String.class);
}
alreadyPresentArgs.add(name);
argGroupBuilder.addArg(optBuilder.build());
}
@ -667,6 +775,8 @@ public final class Picocli {
.map(d -> " Default: " + d + ".")
.ifPresent(transformedDesc::append);
mapper.getEnabledWhen().map(e -> format(" %s.", e)).ifPresent(transformedDesc::append);
// only fully deprecated options, not just deprecated values
mapper.getDeprecatedMetadata()
.filter(deprecatedMetadata -> deprecatedMetadata.getDeprecatedValues().isEmpty())
@ -722,7 +832,7 @@ public final class Picocli {
if (!arg.contains(ARG_KEY_VALUE_SEPARATOR)) {
if (!iterator.hasNext()) {
if (arg.startsWith("--spi")) {
throw new PropertyException(String.format("spi argument %s requires a value.", arg));
throw new PropertyException(format("spi argument %s requires a value.", arg));
}
return args;
}

View file

@ -14,6 +14,10 @@ import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.UnmatchedArgumentException;
import java.util.Optional;
import java.util.function.BooleanSupplier;
import static java.lang.String.format;
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG;
public class ShortErrorMessageHandler implements IParameterExceptionHandler {
@ -23,17 +27,30 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
CommandLine cmd = ex.getCommandLine();
PrintWriter writer = cmd.getErr();
String errorMessage = ex.getMessage();
String additionalSuggestion = null;
if (ex instanceof UnmatchedArgumentException) {
UnmatchedArgumentException uae = (UnmatchedArgumentException) ex;
String[] unmatched = getUnmatchedPartsByOptionSeparator(uae,"=");
String[] unmatched = getUnmatchedPartsByOptionSeparator(uae, "=");
String cliKey = unmatched[0];
PropertyMapper<?> mapper = PropertyMappers.getMapper(cliKey);
if (mapper == null || !(cmd.getCommand() instanceof AbstractCommand)) {
final boolean isDisabledOption = mapper == null && PropertyMappers.isDisabledMapper(cliKey);
final BooleanSupplier isUnknownOption = () -> mapper == null || !(cmd.getCommand() instanceof AbstractCommand);
if (isDisabledOption) {
var enabledWhen = PropertyMappers.getDisabledMapper(cliKey)
.map(PropertyMapper::getEnabledWhen)
.filter(Optional::isPresent)
.map(desc -> format(". %s", desc.get()))
.orElse("");
errorMessage = format("Disabled option: '%s'%s", cliKey, enabledWhen);
additionalSuggestion = "Specify '--help-all' to obtain information on all options and their availability.";
} else if (isUnknownOption.getAsBoolean()) {
if (cliKey.split("\\s").length > 1) {
errorMessage = "Option: '" + cliKey + "' is not expected to contain whitespace, please remove any unnecessary quoting/escaping";
} else {
@ -42,12 +59,13 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
} else {
AbstractCommand command = cmd.getCommand();
if (!command.getOptionCategories().contains(mapper.getCategory())) {
errorMessage = "Option: '" + cliKey + "' not valid for command " + cmd.getCommandName();
errorMessage = format("Option: '%s' not valid for command %s", cliKey, cmd.getCommandName());
} else {
if (Stream.of(args).anyMatch(OPTIMIZED_BUILD_OPTION_LONG::equals) && mapper.isBuildTime() && Start.NAME.equals(cmd.getCommandName())) {
errorMessage = "Build time option: '" + cliKey + "' not usable with pre-built image and --optimized";
errorMessage = format("Build time option: '%s' not usable with pre-built image and --optimized", cliKey);
} else {
errorMessage = (mapper.isRunTime()?"Run time":"Build time") + " option: '" + cliKey + "' not usable with " + cmd.getCommandName();
final var optionType = mapper.isRunTime() ? "Run time" : "Build time";
errorMessage = format("%s option: '%s' not usable with %s", optionType, cliKey, cmd.getCommandName());
}
}
}
@ -59,6 +77,10 @@ public class ShortErrorMessageHandler implements IParameterExceptionHandler {
CommandSpec spec = cmd.getCommandSpec();
writer.printf("Try '%s --help' for more information on the available options.%n", spec.qualifiedName());
if (additionalSuggestion != null) {
writer.println(additionalSuggestion);
}
return getInvalidInputExitCode(ex, cmd);
}

View file

@ -69,4 +69,8 @@ public abstract class AbstractCommand {
}
public abstract String getName();
public CommandLine getCommandLine() {
return spec.commandLine();
}
}

View file

@ -19,23 +19,20 @@ package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.config.ClassLoaderOptions.QUARKUS_REMOVED_ARTIFACTS_PROPERTY;
import static org.keycloak.quarkus.runtime.Environment.getHomePath;
import static org.keycloak.quarkus.runtime.Environment.isDevMode;
import static org.keycloak.quarkus.runtime.Environment.isDevProfile;
import static org.keycloak.quarkus.runtime.cli.Picocli.println;
import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.getAllCliArgs;
import org.keycloak.config.ClassLoaderOptions;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.Messages;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import io.quarkus.bootstrap.runner.QuarkusEntryPoint;
import io.quarkus.bootstrap.runner.RunnerClassLoader;
import io.quarkus.runtime.configuration.ProfileManager;
import io.smallrye.config.ConfigValue;
import org.keycloak.utils.StringUtil;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@ -84,7 +81,7 @@ public final class Build extends AbstractCommand implements Runnable {
beforeReaugmentationOnWindows();
QuarkusEntryPoint.main();
if (!isDevMode()) {
if (!isDevProfile()) {
println(spec.commandLine(), "Server configuration updated and persisted. Run the following command to review the configuration:\n");
println(spec.commandLine(), "\t" + Environment.getCommand() + " show-config\n");
}

View file

@ -20,6 +20,7 @@ package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.exportimport.ExportImportConfig.ACTION_EXPORT;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.configuration.mappers.ExportPropertyMappers;
import picocli.CommandLine.Command;
import java.util.List;
@ -42,6 +43,12 @@ public final class Export extends AbstractExportImportCommand implements Runnabl
optionCategory != OptionCategory.IMPORT).collect(Collectors.toList());
}
@Override
public void validateConfig() {
ExportPropertyMappers.validateConfig();
super.validateConfig();
}
@Override
public String getName() {
return NAME;

View file

@ -23,10 +23,12 @@ import picocli.CommandLine;
public final class HelpAllMixin {
public static final String HELP_ALL_OPTION = "--help-all";
@CommandLine.Spec
private CommandLine.Model.CommandSpec spec;
@CommandLine.Option(names = { "--help-all" }, usageHelp = true, description = "This same help message but with additional options.")
@CommandLine.Option(names = {HELP_ALL_OPTION}, usageHelp = true, description = "This same help message but with additional options.")
public void setHelpAll(boolean allOptions) {
Help help = (Help) spec.commandLine().getHelp();
help.setAllOptions(true);

View file

@ -20,6 +20,7 @@ package org.keycloak.quarkus.runtime.cli.command;
import static org.keycloak.exportimport.ExportImportConfig.ACTION_IMPORT;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.configuration.mappers.ImportPropertyMappers;
import picocli.CommandLine.Command;
import java.util.List;
@ -42,6 +43,12 @@ public final class Import extends AbstractExportImportCommand implements Runnabl
optionCategory != OptionCategory.EXPORT).collect(Collectors.toList());
}
@Override
public void validateConfig() {
ImportPropertyMappers.validateConfig();
super.validateConfig();
}
@Override
public String getName() {
return NAME;

View file

@ -28,6 +28,7 @@ import io.smallrye.config.SmallRyeConfig;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.keycloak.config.Option;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
@ -47,6 +48,22 @@ public final class Configuration {
}
public static boolean isTrue(Option<Boolean> option) {
return getOptionalBooleanValue(NS_KEYCLOAK_PREFIX + option.getKey()).orElse(false);
}
public static boolean contains(Option<?> option, String value) {
return getOptionalValue(NS_KEYCLOAK_PREFIX + option.getKey())
.filter(f -> f.contains(value))
.isPresent();
}
public static boolean equals(Option<?> option, String value) {
return getOptionalValue(NS_KEYCLOAK_PREFIX + option.getKey())
.filter(f -> f.equals(value))
.isPresent();
}
public static synchronized SmallRyeConfig getConfig() {
return (SmallRyeConfig) ConfigProviderResolver.instance().getConfig();
}
@ -222,13 +239,6 @@ public final class Configuration {
}
public static ConfigValue getCurrentBuiltTimeProperty(String name) {
PersistedConfigSource persistedConfigSource = PersistedConfigSource.getInstance();
try {
persistedConfigSource.enable(false);
return getConfigValue(name);
} finally {
persistedConfigSource.enable(true);
}
return PersistedConfigSource.getInstance().runWithDisabled(() -> getConfigValue(name));
}
}

View file

@ -0,0 +1,91 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.keycloak.quarkus.runtime.configuration;
import io.smallrye.config.ConfigSourceInterceptor;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import io.smallrye.config.Priorities;
import jakarta.annotation.Priority;
import org.apache.commons.collections4.iterators.FilterIterator;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import java.util.Iterator;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
/**
* <p>This interceptor is responsible for ignoring disabled Keycloak properties
*
* <p>This interceptor should execute before the {@link PropertyMappingInterceptor} so that disabled properties
* are not mapped to the Quarkus properties.
* <p>
* The reason for the used priority is to always execute the interceptor before default Application Config Source interceptors
* and before the {@link PropertyMappingInterceptor}
*/
@Priority(Priorities.APPLICATION - 20)
public class DisabledMappersInterceptor implements ConfigSourceInterceptor {
private static final ThreadLocal<Boolean> ENABLED = ThreadLocal.withInitial(() -> false);
public static void enable() {
enable(true);
}
public static void disable() {
enable(false);
}
public static void enable(boolean enable) {
ENABLED.set(enable);
}
private <T> boolean isDisabledMapper(String property) {
return property.startsWith(NS_KEYCLOAK_PREFIX) && PropertyMappers.isDisabledMapper(property);
}
Iterator<String> filterDisabledMappers(Iterator<String> iter) {
return new FilterIterator<>(iter, item -> !isDisabledMapper(item));
}
@Override
public Iterator<String> iterateNames(ConfigSourceInterceptorContext context) {
return filterDisabledMappers(context.iterateNames());
}
@Override
public ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
if (isEnabled() && isDisabledMapper(name)) {
return null;
}
return context.proceed(name);
}
public static boolean isEnabled() {
return Boolean.TRUE.equals(ENABLED.get());
}
public static void runWithDisabled(Runnable execution) {
try {
disable();
execution.run();
} finally {
enable();
}
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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.keycloak.quarkus.runtime.configuration;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import picocli.CommandLine;
import java.util.List;
/**
* Custom CommandLine.UnmatchedArgumentException with amended suggestions
*/
public class KcUnmatchedArgumentException extends CommandLine.UnmatchedArgumentException {
public KcUnmatchedArgumentException(CommandLine commandLine, List<String> args) {
super(commandLine, args);
}
@Override
public List<String> getSuggestions() {
// filter out disabled mappers
return super.getSuggestions().stream().filter(f -> !PropertyMappers.isDisabledMapper(f)).toList();
}
}

View file

@ -51,7 +51,7 @@ public final class PersistedConfigSource extends PropertiesConfigSource {
* to ignore this config source. Otherwise, default values are not resolved at runtime because the property will be
* resolved from this config source, if persisted.
*/
private static final ThreadLocal<Boolean> ENABLED = new ThreadLocal<>();
private static final ThreadLocal<Boolean> ENABLED = ThreadLocal.withInitial(() -> true);
private PersistedConfigSource() {
super(readProperties(), "", 200);
@ -146,16 +146,24 @@ public final class PersistedConfigSource extends PropertiesConfigSource {
return null;
}
public void enable(boolean enabled) {
if (enabled) {
ENABLED.remove();
} else {
ENABLED.set(enabled);
}
public void enable() {
ENABLED.set(true);
}
public void disable() {
ENABLED.set(false);
}
private boolean isEnabled() {
Boolean result = ENABLED.get();
return result == null ? true : result;
return Boolean.TRUE.equals(ENABLED.get());
}
public <T> T runWithDisabled(Supplier<T> execution) {
try {
disable();
return execution.get();
} finally {
enable();
}
}
}

View file

@ -20,6 +20,8 @@ import io.smallrye.config.ConfigSourceInterceptor;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import io.smallrye.config.Priorities;
import jakarta.annotation.Priority;
import org.apache.commons.collections4.iterators.FilterIterator;
import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.quarkus.runtime.Environment;
@ -42,8 +44,11 @@ import static org.keycloak.quarkus.runtime.Environment.isRebuild;
* from Keycloak (e.g.: database) is mapped to multiple properties in Quarkus.
*
* <p>This interceptor must execute after the {@link io.smallrye.config.ExpressionConfigSourceInterceptor} so that expressions
* are properly resolved before executing this interceptor. Hence, leaving the default priority.
* are properly resolved before executing this interceptor.
* <p>
* The reason for the used priority is to always execute the interceptor before default Application Config Source interceptors
*/
@Priority(Priorities.APPLICATION - 10)
public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
private static ThreadLocal<Boolean> disable = new ThreadLocal<>();

View file

@ -20,22 +20,30 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import org.keycloak.config.ExportOptions;
import picocli.CommandLine;
import org.keycloak.config.Option;
import org.keycloak.config.OptionBuilder;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import java.util.Optional;
import static org.keycloak.exportimport.ExportImportConfig.PROVIDER;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalValue;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class ExportPropertyMappers {
public final class ExportPropertyMappers {
private static final String EXPORTER_PROPERTY = "kc.spi-export-exporter";
private static final String SINGLE_FILE = "singleFile";
private static final String DIR = "dir";
private ExportPropertyMappers() {
}
public static PropertyMapper<?>[] getMappers() {
return new PropertyMapper[] {
fromOption(ExportOptions.FILE)
.to("kc.spi-export-exporter")
return new PropertyMapper[]{
fromOption(EXPORTER_PLACEHOLDER)
.to(EXPORTER_PROPERTY)
.transformer(ExportPropertyMappers::transformExporter)
.paramLabel("file")
.build(),
@ -49,43 +57,69 @@ final class ExportPropertyMappers {
.build(),
fromOption(ExportOptions.REALM)
.to("kc.spi-export-single-file-realm-name")
.isEnabled(ExportPropertyMappers::isSingleFileProvider)
.paramLabel("realm")
.build(),
fromOption(ExportOptions.REALM)
.to("kc.spi-export-dir-realm-name")
.isEnabled(ExportPropertyMappers::isDirProvider)
.paramLabel("realm")
.build(),
fromOption(ExportOptions.USERS)
.to("kc.spi-export-dir-users-export-strategy")
.isEnabled(ExportPropertyMappers::isDirProvider)
.paramLabel("strategy")
.build(),
fromOption(ExportOptions.USERS_PER_FILE)
.to("kc.spi-export-dir-users-per-file")
.isEnabled(ExportPropertyMappers::isDirProvider)
.paramLabel("number")
.build()
};
}
public static void validateConfig() {
if (getOptionalValue(EXPORTER_PROPERTY).isEmpty() && System.getProperty(PROVIDER) == null) {
throw new PropertyException("Must specify either --dir or --file options.");
}
}
private static final Option<String> EXPORTER_PLACEHOLDER = new OptionBuilder<>("exporter", String.class)
.category(OptionCategory.EXPORT)
.description("Placeholder for determining export mode")
.buildTime(false)
.hidden()
.build();
private static boolean isSingleFileProvider() {
return isProvider(SINGLE_FILE);
}
private static boolean isDirProvider() {
return isProvider(DIR);
}
private static boolean isProvider(String provider) {
return Configuration.getOptionalValue(EXPORTER_PROPERTY)
.filter(provider::equals)
.isPresent();
}
private static Optional<String> transformExporter(Optional<String> option, ConfigSourceInterceptorContext context) {
ConfigValue exporter = context.proceed("kc.spi-export-exporter");
ConfigValue exporter = context.proceed(EXPORTER_PROPERTY);
if (exporter != null) {
return Optional.of(exporter.getValue());
}
if (option.isPresent()) {
return Optional.of("singleFile");
}
ConfigValue dirConfigValue = context.proceed("kc.spi-export-dir-dir");
if (dirConfigValue != null && dirConfigValue.getValue() != null) {
return Optional.of("dir");
}
ConfigValue dirValue = context.proceed("kc.dir");
if (dirValue != null && dirValue.getValue() != null) {
return Optional.of("dir");
}
if (System.getProperty(PROVIDER) == null) {
throw new CommandLine.PicocliException("Must specify either --dir or --file options.");
}
return Optional.empty();
var file = Configuration.getOptionalValue("kc.spi-export-single-file-file").map(f -> SINGLE_FILE);
var dir = Configuration.getOptionalValue("kc.spi-export-dir-dir")
.or(() -> Configuration.getOptionalValue("kc.dir"))
.map(f -> DIR);
// Only one option can be specified
boolean xor = file.isPresent() ^ dir.isPresent();
return xor ? file.or(() -> dir) : Optional.empty();
}
}

View file

@ -20,23 +20,30 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import org.keycloak.config.ImportOptions;
import org.keycloak.config.Option;
import org.keycloak.config.OptionBuilder;
import org.keycloak.config.OptionCategory;
import org.keycloak.exportimport.Strategy;
import picocli.CommandLine;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import java.util.Optional;
import static org.keycloak.exportimport.ExportImportConfig.PROVIDER;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalValue;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class ImportPropertyMappers {
public final class ImportPropertyMappers {
private static final String IMPORTER_PROPERTY = "kc.spi-import-importer";
private static final String SINGLE_FILE = "singleFile";
private static final String DIR = "dir";
private ImportPropertyMappers() {
}
public static PropertyMapper<?>[] getMappers() {
return new PropertyMapper[] {
fromOption(ImportOptions.FILE)
.to("kc.spi-import-importer")
return new PropertyMapper[]{
fromOption(IMPORTER_PLACEHOLDER)
.to(IMPORTER_PROPERTY)
.transformer(ImportPropertyMappers::transformImporter)
.paramLabel("file")
.build(),
@ -51,14 +58,43 @@ final class ImportPropertyMappers {
fromOption(ImportOptions.OVERRIDE)
.to("kc.spi-import-single-file-strategy")
.transformer(ImportPropertyMappers::transformOverride)
.isEnabled(ImportPropertyMappers::isSingleFileProvider)
.build(),
fromOption(ImportOptions.OVERRIDE)
.to("kc.spi-import-dir-strategy")
.transformer(ImportPropertyMappers::transformOverride)
.isEnabled(ImportPropertyMappers::isDirProvider)
.build(),
};
}
public static void validateConfig() {
if (getOptionalValue(IMPORTER_PROPERTY).isEmpty() && System.getProperty(PROVIDER) == null) {
throw new PropertyException("Must specify either --dir or --file options.");
}
}
private static final Option<String> IMPORTER_PLACEHOLDER = new OptionBuilder<>("importer", String.class)
.category(OptionCategory.IMPORT)
.description("Placeholder for determining import mode")
.buildTime(false)
.hidden()
.build();
private static boolean isSingleFileProvider() {
return isProvider(SINGLE_FILE);
}
private static boolean isDirProvider() {
return isProvider(DIR);
}
private static boolean isProvider(String provider) {
return getOptionalValue(IMPORTER_PROPERTY)
.filter(provider::equals)
.isPresent();
}
private static Optional<String> transformOverride(Optional<String> option, ConfigSourceInterceptorContext context) {
if (option.isPresent() && Boolean.parseBoolean(option.get())) {
return Optional.of(Strategy.OVERWRITE_EXISTING.name());
@ -68,25 +104,20 @@ final class ImportPropertyMappers {
}
private static Optional<String> transformImporter(Optional<String> option, ConfigSourceInterceptorContext context) {
ConfigValue importer = context.proceed("kc.spi-import-importer");
ConfigValue importer = context.proceed(IMPORTER_PROPERTY);
if (importer != null) {
return Optional.of(importer.getValue());
}
if (option.isPresent()) {
return Optional.of("singleFile");
}
ConfigValue dirConfigValue = context.proceed("kc.spi-import-dir-dir");
if (dirConfigValue != null && dirConfigValue.getValue() != null) {
return Optional.of("dir");
}
ConfigValue dirValue = context.proceed("kc.dir");
if (dirConfigValue != null && dirValue.getValue() != null) {
return Optional.of("dir");
}
if (System.getProperty(PROVIDER) == null) {
throw new CommandLine.PicocliException("Must specify either --dir or --file options.");
}
return Optional.empty();
var file = getOptionalValue("kc.spi-import-single-file-file").map(f -> SINGLE_FILE);
var dir = getOptionalValue("kc.spi-import-dir-dir")
.or(() -> getOptionalValue("kc.dir"))
.map(f -> DIR);
// Only one option can be specified
boolean xor = file.isPresent() ^ dir.isPresent();
return xor ? file.or(() -> dir) : Optional.empty();
}
}

View file

@ -2,6 +2,7 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import static java.util.Optional.of;
import static org.keycloak.config.LoggingOptions.GELF_ACTIVATED;
import static org.keycloak.quarkus.runtime.configuration.Configuration.isTrue;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException;
@ -18,10 +19,16 @@ import org.keycloak.config.LoggingOptions;
import org.keycloak.quarkus.runtime.Messages;
import io.smallrye.config.ConfigSourceInterceptorContext;
import org.keycloak.quarkus.runtime.configuration.Configuration;
public final class LoggingPropertyMappers {
private LoggingPropertyMappers(){}
private static final String CONSOLE_ENABLED_MSG = "Console log handler is activated";
private static final String FILE_ENABLED_MSG = "File log handler is activated";
private static final String GELF_ENABLED_MSG = "GELF is activated";
private LoggingPropertyMappers() {
}
public static PropertyMapper<?>[] getMappers() {
PropertyMapper<?>[] defaultMappers = new PropertyMapper[]{
@ -29,15 +36,18 @@ public final class LoggingPropertyMappers {
.paramLabel("<handler>")
.build(),
fromOption(LoggingOptions.LOG_CONSOLE_OUTPUT)
.isEnabled(LoggingPropertyMappers::isConsoleEnabled, CONSOLE_ENABLED_MSG)
.to("quarkus.log.console.json")
.paramLabel("output")
.transformer(LoggingPropertyMappers::resolveLogOutput)
.build(),
fromOption(LoggingOptions.LOG_CONSOLE_FORMAT)
.isEnabled(LoggingPropertyMappers::isConsoleEnabled, CONSOLE_ENABLED_MSG)
.to("quarkus.log.console.format")
.paramLabel("format")
.build(),
fromOption(LoggingOptions.LOG_CONSOLE_COLOR)
.isEnabled(LoggingPropertyMappers::isConsoleEnabled, CONSOLE_ENABLED_MSG)
.to("quarkus.log.console.color")
.build(),
fromOption(LoggingOptions.LOG_CONSOLE_ENABLED)
@ -51,15 +61,18 @@ public final class LoggingPropertyMappers {
.transformer(LoggingPropertyMappers.resolveLogHandler("file"))
.build(),
fromOption(LoggingOptions.LOG_FILE)
.isEnabled(LoggingPropertyMappers::isFileEnabled, FILE_ENABLED_MSG)
.to("quarkus.log.file.path")
.paramLabel("file")
.transformer(LoggingPropertyMappers::resolveFileLogLocation)
.build(),
fromOption(LoggingOptions.LOG_FILE_FORMAT)
.isEnabled(LoggingPropertyMappers::isFileEnabled, FILE_ENABLED_MSG)
.to("quarkus.log.file.format")
.paramLabel("<format>")
.build(),
fromOption(LoggingOptions.LOG_FILE_OUTPUT)
.isEnabled(LoggingPropertyMappers::isFileEnabled, FILE_ENABLED_MSG)
.to("quarkus.log.file.json")
.paramLabel("output")
.transformer(LoggingPropertyMappers::resolveLogOutput)
@ -82,52 +95,74 @@ public final class LoggingPropertyMappers {
.transformer(LoggingPropertyMappers.resolveLogHandler("gelf"))
.build(),
fromOption(LoggingOptions.LOG_GELF_LEVEL)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.level")
.paramLabel("level")
.build(),
fromOption(LoggingOptions.LOG_GELF_HOST)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.host")
.paramLabel("hostname")
.build(),
fromOption(LoggingOptions.LOG_GELF_PORT)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.port")
.paramLabel("port")
.build(),
fromOption(LoggingOptions.LOG_GELF_VERSION)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.version")
.paramLabel("version")
.build(),
fromOption(LoggingOptions.LOG_GELF_INCLUDE_STACK_TRACE)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.extract-stack-trace")
.build(),
fromOption(LoggingOptions.LOG_GELF_TIMESTAMP_FORMAT)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.timestamp-pattern")
.paramLabel("pattern")
.build(),
fromOption(LoggingOptions.LOG_GELF_FACILITY)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.facility")
.paramLabel("name")
.build(),
fromOption(LoggingOptions.LOG_GELF_MAX_MSG_SIZE)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.maximum-message-size")
.paramLabel("size")
.build(),
fromOption(LoggingOptions.LOG_GELF_INCLUDE_LOG_MSG_PARAMS)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.include-log-message-parameters")
.build(),
fromOption(LoggingOptions.LOG_GELF_INCLUDE_LOCATION)
.isEnabled(LoggingPropertyMappers::isGelfEnabled, GELF_ENABLED_MSG)
.to("quarkus.log.handler.gelf.include-location")
.build()
};
}
public static boolean isGelfEnabled() {
return isTrue(LoggingOptions.LOG_GELF_ENABLED);
}
public static boolean isConsoleEnabled() {
return isTrue(LoggingOptions.LOG_CONSOLE_ENABLED);
}
public static boolean isFileEnabled() {
return isTrue(LoggingOptions.LOG_FILE_ENABLED);
}
private static BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> resolveLogHandler(String handler) {
return (parentValue, context) -> {
//we want to fall back to console to not have nothing shown up when wrong values are set.
String consoleDependantErrorResult = handler.equals(LoggingOptions.DEFAULT_LOG_HANDLER.name()) ? Boolean.TRUE.toString() : Boolean.FALSE.toString();
String handlers = parentValue.get();
if(handlers.isBlank()) {
if (handlers.isBlank()) {
addInitializationException(Messages.emptyValueForKey("log"));
return of(consoleDependantErrorResult);
}

View file

@ -27,6 +27,7 @@ import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.BooleanSupplier;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
@ -40,12 +41,15 @@ import org.keycloak.quarkus.runtime.cli.PropertyMapperParameterConsumer;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import org.keycloak.utils.StringUtil;
public class PropertyMapper<T> {
static PropertyMapper<?> IDENTITY = new PropertyMapper<>(
new OptionBuilder<>(null, String.class).build(),
null,
() -> false,
"",
null,
null,
null,
@ -59,18 +63,23 @@ public class PropertyMapper<T> {
private final Option<T> option;
private final String to;
private BooleanSupplier enabled;
private String enabledWhen;
private final BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper;
private final String mapFrom;
private final boolean mask;
private final String paramLabel;
private final String envVarFormat;
private String cliFormat;
private BiConsumer<PropertyMapper<T>, ConfigValue> validator;
private final String cliFormat;
private final BiConsumer<PropertyMapper<T>, ConfigValue> validator;
PropertyMapper(Option<T> option, String to, BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper,
PropertyMapper(Option<T> option, String to, BooleanSupplier enabled, String enabledWhen,
BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper,
String mapFrom, String paramLabel, boolean mask, BiConsumer<PropertyMapper<T>, ConfigValue> validator) {
this.option = option;
this.to = to == null ? getFrom() : to;
this.enabled = enabled;
this.enabledWhen = enabledWhen;
this.mapper = mapper == null ? PropertyMapper::defaultTransformer : mapper;
this.mapFrom = mapFrom;
this.paramLabel = paramLabel;
@ -148,15 +157,39 @@ public class PropertyMapper<T> {
return transformedValue;
}
public Option<T> getOption() { return this.option; }
public Option<T> getOption() {
return this.option;
}
public Class<T> getType() { return this.option.getType(); }
public void setEnabled(BooleanSupplier enabled) {
this.enabled = enabled;
}
public boolean isEnabled() {
return enabled.getAsBoolean();
}
public Optional<String> getEnabledWhen() {
return Optional.of(enabledWhen)
.filter(StringUtil::isNotBlank)
.map(e -> "Available only when " + e);
}
public void setEnabledWhen(String enabledWhen) {
this.enabledWhen = enabledWhen;
}
public Class<T> getType() {
return this.option.getType();
}
public String getFrom() {
return MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + this.option.getKey();
}
public String getDescription() { return this.option.getDescription(); }
public String getDescription() {
return this.option.getDescription();
}
public List<String> getExpectedValues() {
return this.option.getExpectedValues();
@ -209,7 +242,11 @@ public class PropertyMapper<T> {
if (mapper == null || (mapFrom == null && name.equals(getFrom()))) {
// no mapper set or requesting a property that does not depend on other property, just return the value from the config source
return ConfigValue.builder().withName(name).withValue(value.orElse(null)).withConfigSourceName(configSourceName).build();
return ConfigValue.builder()
.withName(name)
.withValue(value.orElse(null))
.withConfigSourceName(configSourceName)
.build();
}
Optional<String> mappedValue = mapper.apply(value, context);
@ -218,8 +255,12 @@ public class PropertyMapper<T> {
return null;
}
return ConfigValue.builder().withName(name).withValue(mappedValue.get()).withRawValue(value.orElse(null))
.withConfigSourceName(configSourceName).build();
return ConfigValue.builder()
.withName(name)
.withValue(mappedValue.get())
.withRawValue(value.orElse(null))
.withConfigSourceName(configSourceName)
.build();
}
private ConfigValue convertValue(ConfigValue configValue) {
@ -237,8 +278,10 @@ public class PropertyMapper<T> {
private BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper;
private String mapFrom = null;
private boolean isMasked = false;
private BooleanSupplier isEnabled = () -> true;
private String enabledWhen = "";
private String paramLabel;
private BiConsumer<PropertyMapper<T>, ConfigValue> validator = (mapper, value) -> mapper.validateExpectedValues(value, (c, v) -> mapper.validateSingleValue(c, v));
private BiConsumer<PropertyMapper<T>, ConfigValue> validator = (mapper, value) -> mapper.validateExpectedValues(value, mapper::validateSingleValue);
public Builder(Option<T> option) {
this.option = option;
@ -269,6 +312,17 @@ public class PropertyMapper<T> {
return this;
}
public Builder<T> isEnabled(BooleanSupplier isEnabled, String enabledWhen) {
this.isEnabled = isEnabled;
this.enabledWhen=enabledWhen;
return this;
}
public Builder<T> isEnabled(BooleanSupplier isEnabled) {
this.isEnabled = isEnabled;
return this;
}
public Builder<T> validator(BiConsumer<PropertyMapper<T>, ConfigValue> validator) {
this.validator = validator;
return this;
@ -278,7 +332,7 @@ public class PropertyMapper<T> {
if (paramLabel == null && Boolean.class.equals(option.getType())) {
paramLabel = Boolean.TRUE + "|" + Boolean.FALSE;
}
return new PropertyMapper<T>(option, to, mapper, mapFrom, paramLabel, isMasked, validator);
return new PropertyMapper<T>(option, to, isEnabled, enabledWhen, mapper, mapFrom, paramLabel, isMasked, validator);
}
}
@ -309,7 +363,7 @@ public class PropertyMapper<T> {
}
}
private boolean isCliOption(ConfigValue configValue) {
public static boolean isCliOption(ConfigValue configValue) {
return Optional.ofNullable(configValue.getConfigSourceName()).filter(name -> name.contains(ConfigArgsConfigSource.NAME)).isPresent();
}

View file

@ -3,23 +3,45 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import jakarta.ws.rs.core.MultivaluedHashMap;
import org.jboss.logging.Logger;
import org.keycloak.common.util.CollectionUtil;
import org.keycloak.config.ConfigSupportLevel;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.cli.command.AbstractCommand;
import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.cli.command.ShowConfig;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static org.keycloak.quarkus.runtime.Environment.isParsedCommand;
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
import static org.keycloak.quarkus.runtime.Environment.isRebuildCheck;
public final class PropertyMappers {
public static String VALUE_MASK = "*******";
private static final MappersConfig MAPPERS = new MappersConfig();
private static final Logger log = Logger.getLogger(PropertyMappers.class);
private PropertyMappers(){}
@ -44,8 +66,7 @@ public final class PropertyMappers {
}
public static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
PropertyMapper<?> mapper = MAPPERS.getOrDefault(name, PropertyMapper.IDENTITY);
return mapper.getConfigValue(name, context);
return getMapperOrDefault(name, PropertyMapper.IDENTITY).getConfigValue(name, context);
}
public static boolean isBuildTimeProperty(String name) {
@ -53,7 +74,7 @@ public final class PropertyMappers {
return true;
}
PropertyMapper<?> mapper = MAPPERS.get(name);
final PropertyMapper<?> mapper = getMapperOrDefault(name, null);
boolean isBuildTimeProperty = mapper == null ? false : mapper.isBuildTime();
return isBuildTimeProperty
@ -83,6 +104,27 @@ public final class PropertyMappers {
return MAPPERS.getBuildTimeMappers();
}
public static Map<String, PropertyMapper<?>> getDisabledMappers() {
final var disabledMappers = new HashMap<>(getDisabledBuildTimeMappers());
disabledMappers.putAll(getDisabledRuntimeMappers());
return disabledMappers;
}
public static Map<String, PropertyMapper<?>> getDisabledRuntimeMappers() {
return MAPPERS.getDisabledRuntimeMappers();
}
public static Map<String, PropertyMapper<?>> getDisabledBuildTimeMappers() {
return MAPPERS.getDisabledBuildTimeMappers();
}
/**
* Removes all disabled mappers from the runtime/buildtime mappers
*/
public static void sanitizeDisabledMappers() {
MAPPERS.sanitizeDisabledMappers();
}
public static String formatValue(String property, String value) {
property = removeProfilePrefixIfNeeded(property);
PropertyMapper<?> mapper = getMapper(property);
@ -102,32 +144,95 @@ public final class PropertyMappers {
return property;
}
public static PropertyMapper<?> getMapper(String property) {
if (property.startsWith("%")) {
return MAPPERS.get(property.substring(property.indexOf('.') + 1));
}
return MAPPERS.get(property);
private static PropertyMapper<?> getMapperOrDefault(String property, PropertyMapper<?> defaultMapper) {
final var mappers = MAPPERS.getOrDefault(property, Collections.emptyList());
return switch (mappers.size()) {
case 0 -> defaultMapper;
case 1 -> mappers.get(0);
default -> {
var allowedMappers = filterDeniedCategories(mappers);
yield switch (allowedMappers.size()) {
case 0 -> defaultMapper;
case 1 -> allowedMappers.iterator().next();
default -> {
log.debugf("Duplicated mappers for key '%s'. Used the first found.", property);
yield allowedMappers.iterator().next();
}
};
}
};
}
public static Collection<PropertyMapper<?>> getMappers() {
return MAPPERS.values();
public static PropertyMapper<?> getMapper(String property) {
return getMapperOrDefault(polishProperty(property), null);
}
public static List<PropertyMapper<?>> getMappers(String property) {
return MAPPERS.get(polishProperty(property));
}
public static Set<PropertyMapper<?>> getMappers() {
return MAPPERS.values().stream().flatMap(Collection::stream).collect(Collectors.toSet());
}
public static boolean isSupported(PropertyMapper<?> mapper) {
return mapper.getCategory().getSupportLevel().equals(ConfigSupportLevel.SUPPORTED);
}
private static class MappersConfig extends HashMap<String, PropertyMapper<?>> {
public static Optional<PropertyMapper<?>> getDisabledMapper(String property) {
if (property == null) return Optional.empty();
private Map<OptionCategory, List<PropertyMapper<?>>> buildTimeMappers = new EnumMap<>(OptionCategory.class);
private Map<OptionCategory, List<PropertyMapper<?>>> runtimeTimeMappers = new EnumMap<>(OptionCategory.class);
PropertyMapper<?> mapper = getDisabledBuildTimeMappers().get(property);
if (mapper == null) {
mapper = getDisabledRuntimeMappers().get(property);
}
return Optional.ofNullable(mapper);
}
public static boolean isDisabledMapper(String property) {
final Predicate<String> isDisabledMapper = (p) -> getDisabledMapper(p).isPresent() && getMapper(p) == null;
if (property.startsWith("%")) {
return isDisabledMapper.test(property.substring(property.indexOf('.') + 1));
}
return isDisabledMapper.test(property);
}
private static String polishProperty(String property) {
return property.startsWith("%") ? property.substring(property.indexOf('.') + 1) : property;
}
private static Set<PropertyMapper<?>> filterDeniedCategories(List<PropertyMapper<?>> mappers) {
final var allowedCategories = Environment.getParsedCommand()
.map(AbstractCommand::getOptionCategories)
.map(EnumSet::copyOf)
.orElseGet(() -> EnumSet.allOf(OptionCategory.class));
return mappers.stream().filter(f -> allowedCategories.contains(f.getCategory())).collect(Collectors.toSet());
}
private static class MappersConfig extends MultivaluedHashMap<String, PropertyMapper<?>> {
private final Map<OptionCategory, List<PropertyMapper<?>>> buildTimeMappers = new EnumMap<>(OptionCategory.class);
private final Map<OptionCategory, List<PropertyMapper<?>>> runtimeTimeMappers = new EnumMap<>(OptionCategory.class);
private final Map<String, PropertyMapper<?>> disabledBuildTimeMappers = new HashMap<>();
private final Map<String, PropertyMapper<?>> disabledRuntimeMappers = new HashMap<>();
public void addAll(PropertyMapper<?>[] mappers, BooleanSupplier isEnabled, String enabledWhen) {
Arrays.stream(mappers).forEach(mapper -> {
mapper.setEnabled(isEnabled);
mapper.setEnabledWhen(enabledWhen);
});
addAll(mappers);
}
public void addAll(PropertyMapper<?>[] mappers) {
for (PropertyMapper<?> mapper : mappers) {
super.put(mapper.getTo(), mapper);
super.put(mapper.getFrom(), mapper);
super.put(mapper.getCliFormat(), mapper);
super.put(mapper.getEnvVarFormat(), mapper);
addMapper(mapper);
if (mapper.isBuildTime()) {
addMapperByStage(mapper, buildTimeMappers);
@ -137,22 +242,68 @@ public final class PropertyMappers {
}
}
private void addMapperByStage(PropertyMapper<?> mapper, Map<OptionCategory, List<PropertyMapper<?>>> mappers) {
mappers.computeIfAbsent(mapper.getCategory(),
new Function<OptionCategory, List<PropertyMapper<?>>>() {
@Override
public List<PropertyMapper<?>> apply(OptionCategory c) {
return new ArrayList<>();
}
}).add(mapper);
private static void addMapperByStage(PropertyMapper<?> mapper, Map<OptionCategory, List<PropertyMapper<?>>> mappers) {
mappers.computeIfAbsent(mapper.getCategory(), c -> new ArrayList<>()).add(mapper);
}
@Override
public PropertyMapper<?> put(String key, PropertyMapper<?> value) {
if (containsKey(key)) {
throw new IllegalArgumentException("Duplicated mapper for key [" + key + "]");
public void addMapper(PropertyMapper<?> mapper) {
handleMapper(mapper, this::add);
}
public void removeMapper(PropertyMapper<?> mapper) {
handleMapper(mapper, this::remove);
}
private void remove(String key, PropertyMapper<?> mapper) {
List<PropertyMapper<?>> list = get(key);
if (CollectionUtil.isNotEmpty(list)) {
list.remove(mapper);
}
}
public void sanitizeDisabledMappers() {
if (Environment.getParsedCommand().isEmpty()) return; // do not sanitize when no command is present
DisabledMappersInterceptor.runWithDisabled(() -> { // We need to have the whole configuration available
// Initialize profile in order to check state of features. Disable Persisted CS for re-augmentation
if (isRebuildCheck()) {
PersistedConfigSource.getInstance().runWithDisabled(Environment::getCurrentOrCreateFeatureProfile);
} else {
Environment.getCurrentOrCreateFeatureProfile();
}
sanitizeMappers(buildTimeMappers, disabledBuildTimeMappers);
sanitizeMappers(runtimeTimeMappers, disabledRuntimeMappers);
assertDuplicatedMappers();
});
}
private void assertDuplicatedMappers() {
final var duplicatedMappers = entrySet().stream()
.filter(e -> CollectionUtil.isNotEmpty(e.getValue()))
.filter(e -> e.getValue().size() > 1)
.toList();
final var isBuildPhase = isRebuild() || isRebuildCheck() || isParsedCommand(Build.NAME);
final var allowedForCommand = isParsedCommand(ShowConfig.NAME);
if (!duplicatedMappers.isEmpty()) {
duplicatedMappers.forEach(f -> {
final var filteredMappers = filterDeniedCategories(f.getValue());
if (filteredMappers.size() > 1) {
final var areBuildTimeMappers = filteredMappers.stream().anyMatch(PropertyMapper::isBuildTime);
// thrown in runtime, or in build time, when some mapper is marked as buildTime + not allowed to have duplicates for specific command
final var shouldBeThrown = !allowedForCommand && (!isBuildPhase || areBuildTimeMappers);
if (shouldBeThrown) {
throw new PropertyException(String.format("Duplicated mapper for key '%s'.", f.getKey()));
}
}
});
}
return super.put(key, value);
}
public Map<OptionCategory, List<PropertyMapper<?>>> getRuntimeMappers() {
@ -162,6 +313,35 @@ public final class PropertyMappers {
public Map<OptionCategory, List<PropertyMapper<?>>> getBuildTimeMappers() {
return buildTimeMappers;
}
}
public Map<String, PropertyMapper<?>> getDisabledBuildTimeMappers() {
return disabledBuildTimeMappers;
}
public Map<String, PropertyMapper<?>> getDisabledRuntimeMappers() {
return disabledRuntimeMappers;
}
private static void sanitizeMappers(Map<OptionCategory, List<PropertyMapper<?>>> mappers,
Map<String, PropertyMapper<?>> disabledMappers) {
mappers.forEach((category, propertyMappers) ->
propertyMappers.removeIf(pm -> {
final boolean shouldRemove = !pm.isEnabled();
if (shouldRemove) {
MAPPERS.removeMapper(pm);
handleMapper(pm, disabledMappers::put);
}
return shouldRemove;
}));
}
private static void handleMapper(PropertyMapper<?> mapper, BiConsumer<String, PropertyMapper<?>> operation) {
operation.accept(mapper.getFrom(), mapper);
if (!mapper.getFrom().equals(mapper.getTo())) {
operation.accept(mapper.getTo(), mapper);
}
operation.accept(mapper.getCliFormat(), mapper);
operation.accept(mapper.getEnvVarFormat(), mapper);
}
}
}

View file

@ -16,3 +16,4 @@
#
org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor
org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor

View file

@ -3,3 +3,7 @@ spi-hostname-default-frontend-url = ${keycloak.frontendUrl:http://filepropdefaul
log-level=${SOME_LOG_LEVEL:warn}
config-keystore=src/test/resources/keystore
config-keystore-password=secret
quarkus.log.file.path=random/path
log-gelf-level=WARN

View file

@ -87,7 +87,7 @@ public class OptionValidationTest {
}
@Test
@Launch({"start", "--db-username=foobar","--db-pasword=mytestpw", "--foobar=barfoo"})
@Launch({"start", "--db-username=foobar", "--db-pasword=mytestpw", "--foobar=barfoo"})
public void failWithFirstOptionOnMultipleUnknownOptions(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
assertEquals("Unknown option: '--db-pasword'\n" +
@ -96,7 +96,7 @@ public class OptionValidationTest {
}
@Test
@Launch({ "start", "--db postgres" })
@Launch({"start", "--db postgres"})
void failSingleParamWithSpace(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertError("Option: '--db postgres' is not expected to contain whitespace, please remove any unnecessary quoting/escaping");

View file

@ -38,6 +38,6 @@ public class ExportDistTest {
cliResult.assertNoMessage("Listening on: http");
cliResult = dist.run("export", "--realm=master");
cliResult.assertMessage("Must specify either --dir or --file options.");
cliResult.assertError("Must specify either --dir or --file options.");
}
}

View file

@ -43,5 +43,8 @@ public class ImportDistTest {
cliResult.assertMessage("Import finished successfully");
cliResult.assertNoMessage("Changes detected in configuration");
cliResult.assertNoMessage("Listening on: http");
cliResult = dist.run("import");
cliResult.assertError("Must specify either --dir or --file options.");
}
}

View file

@ -96,6 +96,9 @@ public class LoggingDistTest {
@Launch({ "start-dev", "--log-console-output=json" })
void testJsonFormatApplied(LaunchResult result) throws JsonProcessingException {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("The following used run time options are UNAVAILABLE and will be ignored during build time:");
cliResult.assertMessage("- log-console-output: Available only when Console log handler is activated.");
cliResult.assertJsonLogDefaultsApplied();
cliResult.assertStartedDevMode();
}

View file

@ -20,43 +20,117 @@ package org.keycloak.it.cli.dist;
import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.keycloak.it.junit5.extension.CLIResult;
import org.keycloak.it.junit5.extension.DistributionTest;
import org.keycloak.it.junit5.extension.RawDistOnly;
import org.keycloak.it.junit5.extension.WithEnvVars;
import org.keycloak.it.utils.KeycloakDistribution;
import java.nio.file.Paths;
import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.keycloak.quarkus.runtime.cli.command.Main.CONFIG_FILE_LONG_NAME;
@DistributionTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OptionsDistTest {
@Test
@Order(1)
@Launch({"build", "--db=invalid"})
public void failInvalidOptionValue(LaunchResult result) {
Assertions.assertTrue(result.getErrorOutput().contains("Invalid value for option '--db': invalid. Expected values are: dev-file, dev-mem, mariadb, mssql, mysql, oracle, postgres"));
}
@Test
@Launch({"start-dev", "--test=invalid"})
public void testServerDoesNotStartIfValidationFailDuringReAugStartDev(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Unknown option: '--test'")).count());
}
@Test
@Order(2)
@Launch({"start", "--test=invalid"})
public void testServerDoesNotStartIfValidationFailDuringReAugStart(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Unknown option: '--test'")).count());
}
@Test
@Order(3)
@Launch({"start", "--log=console", "--log-file-output=json", "--http-enabled=true", "--hostname-strict=false"})
public void testServerDoesNotStartIfDisabledFileLogOption(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-file-output'. Available only when File log handler is activated")).count());
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Possible solutions: --log, --log-console-output, --log-console-format, --log-console-color")).count());
}
@Test
@Order(4)
@Launch({"start", "--log=file", "--log-file-output=json", "--http-enabled=true", "--hostname-strict=false"})
public void testServerStartIfEnabledFileLogOption(LaunchResult result) {
assertEquals(0, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-file-output'. Available only when File log handler is activated")).count());
}
@Test
@Order(5)
@WithEnvVars({"KC_LOG", "console", "KC_LOG_CONSOLE_COLOR", "true", "KC_LOG_FILE", "something-env", "KC_LOG_GELF_VERSION", "1.1", "KC_HTTP_ENABLED", "true", "KC_HOSTNAME_STRICT", "false"})
@Launch({"start"})
public void testSettingEnvVars(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("The following used run time options are UNAVAILABLE and will be ignored during build time:");
cliResult.assertMessage("- log-file: Available only when File log handler is activated.");
cliResult.assertMessage("- log-gelf-version: Available only when GELF is activated.");
cliResult.assertMessage("quarkus.log.console.color");
cliResult.assertMessage("config property is deprecated and should not be used anymore");
}
@Test
@Order(6)
@RawDistOnly(reason = "Raw is enough and we avoid issues with including custom conf file in the container")
public void testExpressionsInConfigFile(KeycloakDistribution distribution) {
distribution.setEnvVar("MY_LOG_LEVEL", "debug");
CLIResult result = distribution.run(CONFIG_FILE_LONG_NAME + "=" + Paths.get("src/test/resources/OptionsDistTest/keycloak.conf").toAbsolutePath().normalize(), "start-dev");
result.assertMessage("DEBUG [org.keycloak");
distribution.setEnvVar("MY_LOG_LEVEL", "warn");
CLIResult result = distribution.run(CONFIG_FILE_LONG_NAME + "=" + Paths.get("src/test/resources/OptionsDistTest/keycloak.conf").toAbsolutePath().normalize(), "start", "--http-enabled=true", "--hostname-strict=false", "--optimized");
result.assertNoMessage("INFO [io.quarkus]");
result.assertNoMessage("Listening on:");
// specified in the OptionsDistTest/keycloak.conf
result.assertMessage("The following used run time options are UNAVAILABLE and will be ignored during build time:");
result.assertMessage("- log-gelf-level: Available only when GELF is activated.");
result.assertMessage("- log-gelf-version: Available only when GELF is activated.");
}
@Test
@Order(7)
@Launch({"start", "--log=console", "--log-gelf-include-stack-trace=true"})
public void testDisabledGelfOption(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertError("Disabled option: '--log-gelf-include-stack-trace'. Available only when GELF is activated");
cliResult.assertError("Possible solutions: --log, --log-console-output, --log-console-format, --log-console-color, --log-level");
cliResult.assertError("Try '" + KeycloakDistribution.SCRIPT_CMD + " start --help' for more information on the available options.");
cliResult.assertError("Specify '--help-all' to obtain information on all options and their availability.");
}
// Start-dev should be executed as last tests - build is done for development mode
@Test
@Order(8)
@Launch({"start-dev", "--test=invalid"})
public void testServerDoesNotStartIfValidationFailDuringReAugStartDev(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Unknown option: '--test'")).count());
}
@Test
@Order(9)
@Launch({"start-dev", "--log=console", "--log-file-output=json"})
public void testServerDoesNotStartDevIfDisabledFileLogOption(LaunchResult result) {
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-file-output'. Available only when File log handler is activated")).count());
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Possible solutions: --log, --log-console-output, --log-console-format, --log-console-color")).count());
}
@Test
@Order(10)
@Launch({"start-dev", "--log=file", "--log-file-output=json", "--log-console-color=true"})
public void testServerStartDevIfEnabledFileLogOption(LaunchResult result) {
assertEquals(0, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-file-output'. Available only when File log handler is activated")).count());
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Disabled option: '--log-console-color'. Available only when Console log handler is activated")).count());
assertEquals(1, result.getErrorStream().stream().filter(s -> s.contains("Possible solutions: --log, --log-file, --log-file-format, --log-file-output, --log-level")).count());
}
}

View file

@ -1 +1,8 @@
log-level=${MY_LOG_LEVEL:warn}
log=console,file
log-file-output=json
# Ignored disabled
log-gelf-level=WARN
log-gelf-version=1.1

View file

@ -74,47 +74,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false>
Enable or disable colors when logging to console. Default: false.
Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format>
The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-level <category:level>
The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to

View file

@ -74,47 +74,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false>
Enable or disable colors when logging to console. Default: false.
Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format>
The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. Available
only when File log handler is activated.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
SSS} %-5p [%c] (%t) %s%e%n. Available only when File log handler is
activated.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
values are: default, json. Default: default. Available only when File log
handler is activated.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
Default: keycloak. Available only when GELF is activated.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
Default: localhost. Available only when GELF is activated.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
DEPRECATED. Include source code location. Default: true. Available only when
GELF is activated.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
Default: INFO. Available only when GELF is activated.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
DEPRECATED. The port the Logstash or Graylog Host is called on. Default:
12201. Available only when GELF is activated.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. Available only
when GELF is activated.
--log-gelf-version <version>
The GELF version to be used. Possible values are: 1.0, 1.1. Default: 1.1.
Available only when GELF is activated.
--log-level <category:level>
The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to

View file

@ -74,47 +74,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false>
Enable or disable colors when logging to console. Default: false.
Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format>
The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-level <category:level>
The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to

View file

@ -74,47 +74,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false>
Enable or disable colors when logging to console. Default: false.
Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format>
The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. Available
only when File log handler is activated.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
SSS} %-5p [%c] (%t) %s%e%n. Available only when File log handler is
activated.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
values are: default, json. Default: default. Available only when File log
handler is activated.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
Default: keycloak. Available only when GELF is activated.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
Default: localhost. Available only when GELF is activated.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
DEPRECATED. Include source code location. Default: true. Available only when
GELF is activated.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
Default: INFO. Available only when GELF is activated.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
DEPRECATED. The port the Logstash or Graylog Host is called on. Default:
12201. Available only when GELF is activated.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. Available only
when GELF is activated.
--log-gelf-version <version>
The GELF version to be used. Possible values are: 1.0, 1.1. Default: 1.1.
Available only when GELF is activated.
--log-level <category:level>
The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to

View file

@ -238,47 +238,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false>
Enable or disable colors when logging to console. Default: false.
Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format>
The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-level <category:level>
The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to

View file

@ -238,47 +238,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false>
Enable or disable colors when logging to console. Default: false.
Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format>
The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. Available
only when File log handler is activated.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
SSS} %-5p [%c] (%t) %s%e%n. Available only when File log handler is
activated.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
values are: default, json. Default: default. Available only when File log
handler is activated.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
Default: keycloak. Available only when GELF is activated.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
Default: localhost. Available only when GELF is activated.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
DEPRECATED. Include source code location. Default: true. Available only when
GELF is activated.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
Default: INFO. Available only when GELF is activated.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
DEPRECATED. The port the Logstash or Graylog Host is called on. Default:
12201. Available only when GELF is activated.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. Available only
when GELF is activated.
--log-gelf-version <version>
The GELF version to be used. Possible values are: 1.0, 1.1. Default: 1.1.
Available only when GELF is activated.
--log-level <category:level>
The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to

View file

@ -239,47 +239,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false>
Enable or disable colors when logging to console. Default: false.
Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format>
The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-level <category:level>
The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to

View file

@ -239,47 +239,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false>
Enable or disable colors when logging to console. Default: false.
Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format>
The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. Available
only when File log handler is activated.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
SSS} %-5p [%c] (%t) %s%e%n. Available only when File log handler is
activated.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
values are: default, json. Default: default. Available only when File log
handler is activated.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
Default: keycloak. Available only when GELF is activated.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
Default: localhost. Available only when GELF is activated.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
DEPRECATED. Include source code location. Default: true. Available only when
GELF is activated.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
Default: INFO. Available only when GELF is activated.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
DEPRECATED. The port the Logstash or Graylog Host is called on. Default:
12201. Available only when GELF is activated.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. Available only
when GELF is activated.
--log-gelf-version <version>
The GELF version to be used. Possible values are: 1.0, 1.1. Default: 1.1.
Available only when GELF is activated.
--log-level <category:level>
The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to

View file

@ -177,47 +177,16 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false>
Enable or disable colors when logging to console. Default: false.
Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format>
The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-level <category:level>
The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to

View file

@ -177,47 +177,61 @@ Logging:
--log <handler> Enable one or more log handlers in a comma-separated list. Possible values
are: console, file, gelf (deprecated). Default: console.
--log-console-color <true|false>
Enable or disable colors when logging to console. Default: false.
Enable or disable colors when logging to console. Default: false. Available
only when Console log handler is activated.
--log-console-format <format>
The format of unstructured console log entries. If the format has spaces in
it, escape the value using "<format>". Default: %d{yyyy-MM-dd HH:mm:ss,SSS} %
-5p [%c] (%t) %s%e%n.
-5p [%c] (%t) %s%e%n. Available only when Console log handler is activated.
--log-console-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log.
values are: default, json. Default: default. Available only when Console log
handler is activated.
--log-file <file> Set the log file path and filename. Default: data/log/keycloak.log. Available
only when File log handler is activated.
--log-file-format <format>
Set a format specific to file log entries. Default: %d{yyyy-MM-dd HH:mm:ss,
SSS} %-5p [%c] (%t) %s%e%n.
SSS} %-5p [%c] (%t) %s%e%n. Available only when File log handler is
activated.
--log-file-output <output>
Set the log output to JSON or default (plain) unstructured logging. Possible
values are: default, json. Default: default.
values are: default, json. Default: default. Available only when File log
handler is activated.
--log-gelf-facility <name>
DEPRECATED. The facility (name of the process) that sends the message.
Default: keycloak.
Default: keycloak. Available only when GELF is activated.
--log-gelf-host <hostname>
DEPRECATED. Hostname of the Logstash or Graylog Host. By default UDP is used,
prefix the host with 'tcp:' to switch to TCP. Example: 'tcp:localhost'
Default: localhost.
Default: localhost. Available only when GELF is activated.
--log-gelf-include-location <true|false>
DEPRECATED. Include source code location. Default: true.
DEPRECATED. Include source code location. Default: true. Available only when
GELF is activated.
--log-gelf-include-message-parameters <true|false>
DEPRECATED. Include message parameters from the log event. Default: true.
Available only when GELF is activated.
--log-gelf-include-stack-trace <true|false>
DEPRECATED. If set to true, occuring stack traces are included in the
'StackTrace' field in the GELF output. Default: true.
'StackTrace' field in the GELF output. Default: true. Available only when
GELF is activated.
--log-gelf-level <level>
DEPRECATED. The log level specifying which message levels will be logged by
the GELF logger. Message levels lower than this value will be discarded.
Default: INFO.
Default: INFO. Available only when GELF is activated.
--log-gelf-max-message-size <size>
DEPRECATED. Maximum message size (in bytes). If the message size is exceeded,
GELF will submit the message in multiple chunks. Default: 8192.
GELF will submit the message in multiple chunks. Default: 8192. Available
only when GELF is activated.
--log-gelf-port <port>
DEPRECATED. The port the Logstash or Graylog Host is called on. Default: 12201.
DEPRECATED. The port the Logstash or Graylog Host is called on. Default:
12201. Available only when GELF is activated.
--log-gelf-timestamp-format <pattern>
DEPRECATED. Set the format for the GELF timestamp field. Uses Java
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS.
SimpleDateFormat pattern. Default: yyyy-MM-dd HH:mm:ss,SSS. Available only
when GELF is activated.
--log-gelf-version <version>
The GELF version to be used. Possible values are: 1.0, 1.1. Default: 1.1.
Available only when GELF is activated.
--log-level <category:level>
The log level of the root category or a comma-separated list of individual
categories and their levels. For the root category, you don't need to

View file

@ -20,14 +20,23 @@ import java.util.Collection;
public class StringUtil {
/**
* Returns true if string is null or blank
*/
public static boolean isBlank(String str) {
return !(isNotBlank(str));
}
public static boolean isNotBlank(String str) {
return str != null && !"".equals(str.trim());
/**
* Returns true if string is not null and not blank
*/
public static boolean isNotBlank(String str) {
return str != null && !str.isBlank();
}
/**
* Returns true if string is null or empty
*/
public static boolean isNullOrEmpty(String str) {
return str == null || str.isEmpty();
}