org.keycloak.quarkus.runtime.cli.Picocli Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2020 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.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.OptionRenderer.decorateDuplicitOptionName;
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG;
import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.parseConfigArgs;
import static org.keycloak.quarkus.runtime.configuration.Configuration.OPTION_PART_SEPARATOR;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getBuildTimeProperty;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfig;
import static org.keycloak.quarkus.runtime.Environment.isDevMode;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getCurrentBuiltTimeProperty;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getRawPersistedProperty;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getRuntimeProperty;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.maskValue;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.isBuildTimeProperty;
import static org.keycloak.utils.StringUtil.isNotBlank;
import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST;
import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.regex.Pattern;
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.BootstrapAdmin;
import org.keycloak.quarkus.runtime.cli.command.Build;
import org.keycloak.quarkus.runtime.cli.command.Main;
import org.keycloak.quarkus.runtime.cli.command.ShowConfig;
import org.keycloak.quarkus.runtime.cli.command.Start;
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;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
import org.keycloak.quarkus.runtime.Environment;
import io.smallrye.config.ConfigValue;
import picocli.CommandLine;
import picocli.CommandLine.ParameterException;
import picocli.CommandLine.ParseResult;
import picocli.CommandLine.DuplicateOptionAnnotationsException;
import picocli.CommandLine.Help.Ansi;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Model.ISetter;
import picocli.CommandLine.Model.OptionSpec;
import picocli.CommandLine.Model.ArgGroupSpec;
public class Picocli {
public static final String ARG_PREFIX = "--";
public static final String ARG_SHORT_PREFIX = "-";
public static final String NO_PARAM_LABEL = "none";
private static class IncludeOptions {
boolean includeRuntime;
boolean includeBuildTime;
}
public void parseAndRun(List cliArgs) {
// perform two passes over the cli args. First without option validation to determine the current command, then with option validation enabled
CommandLine cmd = createCommandLine(spec -> spec
.addUnmatchedArgsBinding(CommandLine.Model.UnmatchedArgsBinding.forStringArrayConsumer(new ISetter() {
@Override
public T set(T value) throws Exception {
return null; // just ignore
}
})));
String[] argArray = cliArgs.toArray(new String[0]);
try {
ParseResult result = cmd.parseArgs(argArray); // process the cli args first to init the config file and perform validation
var commandLineList = result.asCommandLineList();
// recreate the command specifically for the current
cmd = createCommandLineForCommand(cliArgs, commandLineList);
int exitCode;
if (isRebuildCheck()) {
CommandLine currentCommand = null;
if (commandLineList.size() > 1) {
currentCommand = commandLineList.get(commandLineList.size() - 1);
}
exitCode = runReAugmentationIfNeeded(cliArgs, cmd, currentCommand);
} else {
PropertyMappers.sanitizeDisabledMappers();
exitCode = run(cmd, argArray);
}
exitOnFailure(exitCode, cmd);
} catch (ParameterException parEx) {
catchParameterException(parEx, cmd, argArray);
} catch (ProfileException | PropertyException proEx) {
catchProfileException(proEx.getMessage(), proEx.getCause(), cmd);
}
}
protected int run(CommandLine cmd, String[] argArray) {
return cmd.execute(argArray);
}
private CommandLine createCommandLineForCommand(List cliArgs, List commandLineList) {
return createCommandLine(spec -> {
// use the incoming commandLineList from the initial parsing to determine the current command
CommandSpec currentSpec = spec;
// add help to the root and all commands as it is not inherited
addHelp(currentSpec);
for (CommandLine commandLine : commandLineList.subList(1, commandLineList.size())) {
CommandLine subCommand = currentSpec.subcommands().get(commandLine.getCommandName());
if (subCommand == null) {
currentSpec = null;
break;
}
currentSpec = subCommand.getCommandSpec();
addHelp(currentSpec);
}
if (currentSpec != null) {
addCommandOptions(cliArgs, currentSpec.commandLine());
}
if (isRebuildCheck()) {
// build command should be available when running re-aug
addCommandOptions(cliArgs, spec.subcommands().get(Build.NAME));
}
});
}
private 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 void catchProfileException(String message, Throwable cause, CommandLine cmd) {
ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler();
errorHandler.error(cmd.getErr(), message, cause);
exitOnFailure(CommandLine.ExitCode.USAGE, cmd);
}
protected 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.
System.exit(exitCode);
}
}
protected int runReAugmentationIfNeeded(List cliArgs, CommandLine cmd, CommandLine currentCommand) {
int exitCode = 0;
if (currentCommand == null) {
return exitCode; // possible if using --version or the user made a mistake
}
String currentCommandName = currentCommand.getCommandName();
if (shouldSkipRebuild(cliArgs, currentCommandName)) {
return exitCode;
}
if (currentCommandName.equals(StartDev.NAME)) {
String profile = org.keycloak.common.util.Environment.getProfile();
if (profile == null) {
// force the server image to be set with the dev profile
Environment.forceDevProfile();
}
}
if (requiresReAugmentation(currentCommand)) {
PropertyMappers.sanitizeDisabledMappers();
exitCode = runReAugmentation(cliArgs, cmd);
}
return exitCode;
}
private static boolean shouldSkipRebuild(List cliArgs, String currentCommandName) {
return cliArgs.contains("--help")
|| cliArgs.contains("-h")
|| cliArgs.contains("--help-all")
|| currentCommandName.equals(Build.NAME)
|| currentCommandName.equals(ShowConfig.NAME)
|| currentCommandName.equals(BootstrapAdmin.NAME)
|| currentCommandName.equals(Tools.NAME);
}
private static boolean requiresReAugmentation(CommandLine cmdCommand) {
if (ConfigArgsConfigSource.getAllCliArgs().contains(Start.NAME)
// run time dev mode is not set
&& !org.keycloak.common.util.Environment.isDevMode()
// build time dev mode was set
&& org.keycloak.common.util.Environment.DEV_PROFILE_VALUE.equals(getBuildTimeProperty(org.keycloak.common.util.Environment.PROFILE).orElse(null))) {
return true;
}
if (hasConfigChanges(cmdCommand)) {
if (!ConfigArgsConfigSource.getAllCliArgs().contains(StartDev.NAME) && "dev".equals(getConfig().getOptionalValue("kc.profile", String.class).orElse(null))) {
return false;
}
return true;
}
return hasProviderChanges();
}
/**
* checks the raw cli input for possible credentials / properties which should be masked,
* and masks them.
* @return a list of potentially masked properties in CLI format, e.g. `--db-password=*******`
* instead of the actual passwords value.
*/
private static List getSanitizedRuntimeCliOptions() {
List properties = new ArrayList<>();
parseConfigArgs(ConfigArgsConfigSource.getAllCliArgs(), new BiConsumer() {
@Override
public void accept(String key, String value) {
PropertyMapper> mapper = PropertyMappers.getMapper(key);
if (mapper == null || mapper.isRunTime()) {
properties.add(key + "=" + maskValue(key, value));
}
}
}, arg -> {
properties.add(arg);
});
return properties;
}
private static int runReAugmentation(List cliArgs, CommandLine cmd) {
if(!isDevMode() && cmd != null) {
cmd.getOut().println("Changes detected in configuration. Updating the server image.");
checkChangesInBuildOptionsDuringAutoBuild();
}
List configArgsList = new ArrayList<>();
configArgsList.add(Build.NAME);
parseConfigArgs(cliArgs, (k, v) -> {
PropertyMapper> mapper = PropertyMappers.getMapper(k);
if (mapper != null && mapper.isBuildTime()) {
configArgsList.add(k + "=" + v);
}
}, ignored -> {});
int exitCode = cmd.execute(configArgsList.toArray(new String[0]));
if(!isDevMode() && exitCode == cmd.getCommandSpec().exitCodeOnSuccess()) {
cmd.getOut().printf("Next time you run the server, just run:%n%n\t%s %s %s%n%n", Environment.getCommand(), String.join(" ", getSanitizedRuntimeCliOptions()), OPTIMIZED_BUILD_OPTION_LONG);
}
return exitCode;
}
private static boolean hasProviderChanges() {
Map persistedProps = PersistedConfigSource.getInstance().getProperties();
Map deployedProviders = Environment.getProviderFiles();
if (persistedProps.isEmpty()) {
return !deployedProviders.isEmpty();
}
Set providerKeys = persistedProps.keySet().stream().filter(Picocli::isProviderKey).collect(Collectors.toSet());
if (deployedProviders.size() != providerKeys.size()) {
return true;
}
for (String key : providerKeys) {
String fileName = key.substring("kc.provider.file".length() + 1, key.lastIndexOf('.'));
if (!deployedProviders.containsKey(fileName)) {
return true;
}
File file = deployedProviders.get(fileName);
String lastModified = persistedProps.get(key);
if (!lastModified.equals(String.valueOf(file.lastModified()))) {
return true;
}
}
return false;
}
/**
* Additional validation and handling of deprecated options
*
* @param cliArgs
* @param abstractCommand
*/
public static void validateConfig(List cliArgs, AbstractCommand abstractCommand) {
IncludeOptions options = getIncludeOptions(cliArgs, abstractCommand, abstractCommand.getName());
if (!options.includeBuildTime && !options.includeRuntime) {
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
DisabledMappersInterceptor.disable(); // we want all properties, even disabled ones
final List ignoredBuildTime = new ArrayList<>();
final List ignoredRunTime = new ArrayList<>();
final Set disabledBuildTime = new HashSet<>();
final Set disabledRunTime = new HashSet<>();
final Set deprecatedInUse = new HashSet<>();
final Set> disabledMappers = new HashSet<>();
if (options.includeBuildTime) {
disabledMappers.addAll(PropertyMappers.getDisabledBuildTimeMappers().values());
}
if (options.includeRuntime) {
disabledMappers.addAll(PropertyMappers.getDisabledRuntimeMappers().values());
}
checkSpiOptions(options, ignoredBuildTime, ignoredRunTime);
for (OptionCategory category : abstractCommand.getOptionCategories()) {
List> 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) {
ConfigValue configValue = Configuration.getConfigValue(mapper.getFrom());
String configValueStr = configValue.getValue();
// don't consider missing or anything below standard env properties
if (configValueStr == null || configValue.getConfigSourceOrdinal() < 300) {
continue;
}
if (disabledMappers.contains(mapper)) {
if (!PropertyMappers.isDisabledMapper(mapper.getFrom())) {
continue; // we found enabled mapper with the same name
}
// only check build-time for a rebuild, we'll check the runtime later
if (!mapper.isRunTime() || !isRebuild()) {
if (PropertyMapper.isCliOption(configValue)) {
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine(), List.of(mapper.getCliFormat()));
} else {
handleDisabled(mapper.isRunTime() ? disabledRunTime : disabledBuildTime, mapper);
}
}
continue;
}
if (mapper.isBuildTime() && !options.includeBuildTime) {
String currentValue = getRawPersistedProperty(mapper.getFrom()).orElse(null);
if (!configValueStr.equals(currentValue)) {
ignoredBuildTime.add(mapper.getFrom());
continue;
}
}
if (mapper.isRunTime() && !options.includeRuntime) {
ignoredRunTime.add(mapper.getFrom());
continue;
}
mapper.validate(configValue);
mapper.getDeprecatedMetadata().ifPresent(metadata -> {
handleDeprecated(deprecatedInUse, mapper, configValueStr, metadata);
});
}
}
Logger logger = Logger.getLogger(Picocli.class); // logger can't be instantiated in a class field
if (!ignoredBuildTime.isEmpty()) {
throw new PropertyException(format("The following build time options have values that differ from what is persisted - the new values will NOT be used until another build is run: %s\n",
String.join(", ", ignoredBuildTime)));
} else if (!ignoredRunTime.isEmpty()) {
logger.warn(format("The following run time options were found, but will be ignored during build time: %s\n",
String.join(", ", ignoredRunTime)));
}
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 or their behaviour changed in a future release:\n" + String.join("\n", deprecatedInUse) + "\nConsult the Release Notes for details.");
}
} finally {
DisabledMappersInterceptor.enable(disabledMappersInterceptorEnabled);
PropertyMappingInterceptor.enable();
}
}
private static void checkSpiOptions(IncludeOptions options, final List ignoredBuildTime,
final List ignoredRunTime) {
String kcSpiPrefix = NS_KEYCLOAK_PREFIX + "spi";
for (String key : Configuration.getConfig().getPropertyNames()) {
if (!key.startsWith(kcSpiPrefix)) {
continue;
}
boolean buildTimeOption = key.endsWith("-provider") || key.endsWith("-provider-default") || key.endsWith("-enabled");
ConfigValue configValue = Configuration.getConfigValue(key);
String configValueStr = configValue.getValue();
// don't consider missing or anything below standard env properties
if (configValueStr == null || configValue.getConfigSourceOrdinal() < 300) {
continue;
}
if (!options.includeBuildTime) {
if (buildTimeOption) {
String currentValue = getRawPersistedProperty(key).orElse(null);
if (!configValueStr.equals(currentValue)) {
ignoredBuildTime.add(key);
}
}
} else if (!options.includeRuntime && !buildTimeOption) {
ignoredRunTime.add(key);
}
}
}
private static void handleDeprecated(Set deprecatedInUse, PropertyMapper> mapper, String configValue,
DeprecatedMetadata metadata) {
Set deprecatedValuesInUse = new HashSet<>();
if (!metadata.getDeprecatedValues().isEmpty()) {
deprecatedValuesInUse.addAll(Arrays.asList(configValue.split(",")));
deprecatedValuesInUse.retainAll(metadata.getDeprecatedValues());
if (deprecatedValuesInUse.isEmpty()) {
return; // no deprecated values are used, don't emit any warning
}
}
String optionName = mapper.getFrom();
if (optionName.startsWith(NS_KEYCLOAK_PREFIX)) {
optionName = optionName.substring(NS_KEYCLOAK_PREFIX.length());
}
StringBuilder sb = new StringBuilder("\t- ");
sb.append(optionName);
if (!deprecatedValuesInUse.isEmpty()) {
sb.append("=").append(String.join(",", deprecatedValuesInUse));
}
if (metadata.getNote() != null || !metadata.getNewOptionsKeys().isEmpty()) {
sb.append(":");
}
if (metadata.getNote() != null) {
sb.append(" ");
sb.append(metadata.getNote());
if (!metadata.getNote().endsWith(".")) {
sb.append(".");
}
}
if (!metadata.getNewOptionsKeys().isEmpty()) {
sb.append(" Use ");
sb.append(String.join(", ", metadata.getNewOptionsKeys()));
sb.append(".");
}
deprecatedInUse.add(sb.toString());
}
private static void handleDisabled(Set 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 outputDisabledProperties(Set 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) {
Optional currentProfile = ofNullable(org.keycloak.common.util.Environment.getProfile());
Optional persistedProfile = getBuildTimeProperty("kc.profile");
if (!persistedProfile.orElse("").equals(currentProfile.orElse(""))) {
return true;
}
for (String propertyName : getConfig().getPropertyNames()) {
// only check keycloak build-time properties
if (!isBuildTimeProperty(propertyName)) {
continue;
}
ConfigValue configValue = getConfig().getConfigValue(propertyName);
if (configValue == null || configValue.getConfigSourceName() == null) {
continue;
}
// try to resolve any property set using profiles
if (propertyName.startsWith("%")) {
propertyName = propertyName.substring(propertyName.indexOf('.') + 1);
}
String persistedValue = getBuildTimeProperty(propertyName).orElse("");
String runtimeValue = getRuntimeProperty(propertyName).orElse(null);
// compare only the relevant options for this command, as not all options might be set for this command
if (cmdCommand.getCommand() instanceof AbstractCommand) {
AbstractCommand abstractCommand = cmdCommand.getCommand();
PropertyMapper> mapper = PropertyMappers.getMapper(propertyName);
if (mapper != null) {
if (!abstractCommand.getOptionCategories().contains(mapper.getCategory())) {
continue;
}
}
}
if (runtimeValue == null && isNotBlank(persistedValue)) {
PropertyMapper> mapper = PropertyMappers.getMapper(propertyName);
if (mapper != null && persistedValue.equals(Option.getDefaultValueString(mapper.getDefaultValue().orElse(null)))) {
// same as default
continue;
}
// probably because it was unset
return true;
}
// changes to a single property is enough to indicate changes to configuration
if (!persistedValue.equals(runtimeValue)) {
return true;
}
}
//check for defined quarkus raw build properties for UserStorageProvider extensions
if (QuarkusPropertiesConfigSource.getConfigurationFile() != null) {
Optional quarkusPropertiesConfigSource = getConfig().getConfigSource(QuarkusPropertiesConfigSource.NAME);
if (quarkusPropertiesConfigSource.isPresent()) {
Map foundQuarkusBuildProperties = findSupportedRawQuarkusBuildProperties(quarkusPropertiesConfigSource.get().getProperties().entrySet());
//only check if buildProps are found in quarkus properties file.
if (!foundQuarkusBuildProperties.isEmpty()) {
Optional persistedConfigSource = getConfig().getConfigSource(PersistedConfigSource.NAME);
if(persistedConfigSource.isPresent()) {
for(String key : foundQuarkusBuildProperties.keySet()) {
if (notContainsKey(persistedConfigSource.get(), key)) {
//if persisted cs does not contain raw quarkus key from quarkus.properties, assume build is needed as the key is new.
return true;
}
}
//if it contains the key, check if the value actually changed from the persisted one.
return hasAtLeastOneChangedBuildProperty(foundQuarkusBuildProperties, persistedConfigSource.get().getProperties().entrySet());
}
}
}
}
return false;
}
private static boolean hasAtLeastOneChangedBuildProperty(Map foundQuarkusBuildProperties, Set> persistedEntries) {
for(Map.Entry persistedEntry : persistedEntries) {
if (foundQuarkusBuildProperties.containsKey(persistedEntry.getKey())) {
return isChangedValue(foundQuarkusBuildProperties, persistedEntry);
}
}
return false;
}
private static boolean notContainsKey(ConfigSource persistedConfigSource, String key) {
return !persistedConfigSource.getProperties().containsKey(key);
}
private static Map findSupportedRawQuarkusBuildProperties(Set> entries) {
Pattern buildTimePattern = Pattern.compile(QuarkusPropertiesConfigSource.QUARKUS_DATASOURCE_BUILDTIME_REGEX);
Map result = new HashMap<>();
for(Map.Entry entry : entries) {
if (buildTimePattern.matcher(entry.getKey()).matches()) {
result.put(entry.getKey(), entry.getValue());
}
}
return result;
}
private static boolean isChangedValue(Map foundQuarkusBuildProps, Map.Entry persistedEntry) {
return !foundQuarkusBuildProps.get(persistedEntry.getKey()).equals(persistedEntry.getValue());
}
private static boolean isProviderKey(String key) {
return key.startsWith("kc.provider.file");
}
public CommandLine createCommandLine(Consumer consumer) {
CommandSpec spec = CommandSpec.forAnnotatedObject(new Main()).name(Environment.getCommand());
consumer.accept(spec);
CommandLine cmd = new CommandLine(spec);
cmd.setExecutionExceptionHandler(new ExecutionExceptionHandler());
cmd.setParameterExceptionHandler(new ShortErrorMessageHandler());
cmd.setHelpFactory(new HelpFactory());
cmd.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST, new SubCommandListRenderer());
cmd.setErr(getErrWriter());
return cmd;
}
protected PrintWriter getErrWriter() {
return new PrintWriter(System.err, true);
}
private static void addHelp(CommandSpec currentSpec) {
try {
currentSpec.addOption(OptionSpec.builder(Help.OPTION_NAMES)
.usageHelp(true)
.description("This help message.")
.build());
} catch (DuplicateOptionAnnotationsException e) {
// Completion is inheriting mixinStandardHelpOptions = true
}
}
private static IncludeOptions getIncludeOptions(List cliArgs, AbstractCommand abstractCommand, String commandName) {
IncludeOptions result = new IncludeOptions();
if (abstractCommand == null) {
return result;
}
result.includeRuntime = abstractCommand.includeRuntime();
result.includeBuildTime = abstractCommand.includeBuildTime();
if (!result.includeBuildTime && !result.includeRuntime) {
return result;
} else if (result.includeRuntime && !result.includeBuildTime && !ShowConfig.NAME.equals(commandName)) {
result.includeBuildTime = isRebuilt() || !cliArgs.contains(OPTIMIZED_BUILD_OPTION_LONG);
} else if (result.includeBuildTime && !result.includeRuntime) {
result.includeRuntime = isRebuildCheck();
}
return result;
}
private static void addCommandOptions(List cliArgs, CommandLine command) {
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);
}
}
private static void addOptionsToCli(CommandLine commandLine, IncludeOptions includeOptions) {
final Map>> mappers = new EnumMap<>(OptionCategory.class);
// Since we can't run sanitizeDisabledMappers sooner, PropertyMappers.getRuntime|BuildTimeMappers() at this point
// contain both enabled and disabled mappers. Actual filtering is done later (help command, validations etc.).
if (includeOptions.includeRuntime) {
mappers.putAll(PropertyMappers.getRuntimeMappers());
}
if (includeOptions.includeBuildTime) {
combinePropertyMappers(mappers, PropertyMappers.getBuildTimeMappers());
}
addMappedOptionsToArgGroups(commandLine, mappers);
}
private static >>> void combinePropertyMappers(T origMappers, T additionalMappers) {
for (var entry : additionalMappers.entrySet()) {
final List> result = origMappers.getOrDefault(entry.getKey(), new ArrayList<>());
result.addAll(entry.getValue());
origMappers.put(entry.getKey(), result);
}
}
private static void addMappedOptionsToArgGroups(CommandLine commandLine, Map>> propertyMappers) {
CommandSpec cSpec = commandLine.getCommandSpec();
for (OptionCategory category : ((AbstractCommand) commandLine.getCommand()).getOptionCategories()) {
List> mappersInCategory = propertyMappers.get(category);
if (mappersInCategory == null) {
//picocli raises an exception when an ArgGroup is empty, so ignore it when no mappings found for a category.
continue;
}
ArgGroupSpec.Builder argGroupBuilder = ArgGroupSpec.builder()
.heading(category.getHeading() + ":")
.order(category.getOrder())
.validate(false);
final Set alreadyPresentArgs = new HashSet<>();
for (PropertyMapper> mapper : mappersInCategory) {
String name = mapper.getCliFormat();
// Picocli doesn't allow to have multiple options with the same name. We need this in help-all which also prints
// currently disabled options which might have a duplicate among enabled options. This is to register the disabled
// options with a unique name in Picocli. To keep it simple, it adds just a suffix to the options, i.e. there cannot
// be more that 1 disabled option with a unique name.
if (cSpec.optionsMap().containsKey(name)) {
name = decorateDuplicitOptionName(name);
}
String description = mapper.getDescription();
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;
}
OptionSpec.Builder optBuilder = OptionSpec.builder(name)
.description(getDecoratedOptionDescription(mapper))
.paramLabel(mapper.getParamLabel())
.completionCandidates(new Iterable() {
@Override
public Iterator iterator() {
return mapper.getExpectedValues().iterator();
}
})
.hidden(mapper.isHidden());
if (mapper.getDefaultValue().isPresent()) {
optBuilder.defaultValue(Option.getDefaultValueString(mapper.getDefaultValue().get()));
}
if (mapper.getType() != null) {
optBuilder.type(mapper.getType());
if (mapper.isList()) {
// make picocli aware of the only list convention we allow
optBuilder.splitRegex(",");
} else if (mapper.getType().isEnum()) {
// prevent the auto-conversion that picocli does
// we validate the expected values later
optBuilder.type(String.class);
}
} else {
optBuilder.type(String.class);
}
alreadyPresentArgs.add(name);
argGroupBuilder.addArg(optBuilder.build());
}
if (argGroupBuilder.args().isEmpty()) {
continue;
}
cSpec.addArgGroup(argGroupBuilder.build());
}
}
private static String getDecoratedOptionDescription(PropertyMapper> mapper) {
StringBuilder transformedDesc = new StringBuilder(mapper.getDescription());
if (mapper.getType() != Boolean.class && !mapper.getExpectedValues().isEmpty()) {
List decoratedExpectedValues = mapper.getExpectedValues().stream().map(value -> {
if (mapper.getDeprecatedMetadata().isPresent() && mapper.getDeprecatedMetadata().get().getDeprecatedValues().contains(value)) {
return value + " (deprecated)";
}
return value;
}).toList();
var isStrictExpectedValues = mapper.getOption().isStrictExpectedValues();
var printableValues = String.join(", ", decoratedExpectedValues) + (!isStrictExpectedValues ? ", or a custom one" : "");
transformedDesc.append(String.format(" Possible values are: %s.", printableValues));
}
mapper.getDefaultValue()
.map(d -> Option.getDefaultValueString(d).replaceAll("%", "%%")) // escape formats
.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())
.ifPresent(deprecatedMetadata -> {
List deprecatedDetails = new ArrayList<>();
String note = deprecatedMetadata.getNote();
if (note != null) {
if (!note.endsWith(".")) {
note += ".";
}
deprecatedDetails.add(note);
}
if (!deprecatedMetadata.getNewOptionsKeys().isEmpty()) {
String s = deprecatedMetadata.getNewOptionsKeys().size() > 1 ? "s" : "";
deprecatedDetails.add("Use the following option" + s + " instead: " + String.join(", ", deprecatedMetadata.getNewOptionsKeys()) + ".");
}
transformedDesc.insert(0, "@|bold DEPRECATED.|@ ");
if (!deprecatedDetails.isEmpty()) {
transformedDesc
.append(" @|bold ")
.append(String.join(" ", deprecatedDetails))
.append("|@");
}
});
return transformedDesc.toString();
}
public static void println(CommandLine cmd, String message) {
cmd.getOut().println(message);
}
public static List parseArgs(String[] rawArgs) throws PropertyException {
if (rawArgs.length == 0) {
return List.of();
}
// makes sure cli args are available to the config source
ConfigArgsConfigSource.setCliArgs(rawArgs);
// TODO: ignore properties for providers for now, need to fetch them from the providers, otherwise CLI will complain about invalid options
// also ignores system properties as they are set when starting the JVM
// change this once we are able to obtain properties from providers
List args = new ArrayList<>();
ConfigArgsConfigSource.parseConfigArgs(List.of(rawArgs), (arg, value) -> {
if (!arg.startsWith(ConfigArgsConfigSource.SPI_OPTION_PREFIX) && !arg.startsWith("-D")) {
args.add(arg + "=" + value);
}
}, arg -> {
if (arg.startsWith(ConfigArgsConfigSource.SPI_OPTION_PREFIX)) {
throw new PropertyException(format("spi argument %s requires a value.", arg));
}
if (!arg.startsWith("-D")) {
args.add(arg);
}
});
return args;
}
private static void checkChangesInBuildOptionsDuringAutoBuild() {
if (Configuration.isOptimized()) {
List> buildOptions = stream(Configuration.getPropertyNames(true).spliterator(), false)
.sorted()
.map(PropertyMappers::getMapper)
.filter(Objects::nonNull).collect(Collectors.toList());
if (buildOptions.isEmpty()) {
return;
}
StringBuilder options = new StringBuilder();
for (PropertyMapper> mapper : buildOptions) {
String newValue = ofNullable(getCurrentBuiltTimeProperty(mapper.getFrom()))
.map(ConfigValue::getValue)
.orElse("");
String currentValue = getRawPersistedProperty(mapper.getFrom()).get();
if (newValue.equals(currentValue)) {
continue;
}
String name = mapper.getOption().getKey();
options.append("\n\t- ")
.append(name).append("=").append(currentValue)
.append(" > ")
.append(name).append("=").append(newValue);
}
if (options.length() > 0) {
System.out.println(
Ansi.AUTO.string(
new StringBuilder("@|bold,red ")
.append("The previous optimized build will be overridden with the following build options:")
.append(options)
.append("\nTo avoid that, run the 'build' command again and then start the optimized server instance using the '--optimized' flag.")
.append("|@").toString()
)
);
}
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy