diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/ConfigurationProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/ConfigurationProcessor.java index 429476e4da919..d1025b66cbed4 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/ConfigurationProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/menu/ConfigurationProcessor.java @@ -2,15 +2,22 @@ import static io.quarkus.vertx.http.deployment.devmode.console.ConfigEditorProcessor.cleanUpAsciiDocIfNecessary; import static io.quarkus.vertx.http.deployment.devmode.console.ConfigEditorProcessor.isSetByDevServices; +import static io.smallrye.config.Expressions.withoutExpansion; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.config.spi.ConfigSource; + import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.ConfigDescriptionBuildItem; @@ -22,12 +29,18 @@ import io.quarkus.devui.spi.page.Page; import io.quarkus.vertx.http.deployment.devmode.console.ConfigEditorProcessor; import io.quarkus.vertx.http.runtime.devmode.ConfigDescription; +import io.quarkus.vertx.http.runtime.devmode.ConfigSourceName; +import io.smallrye.config.ConfigValue; +import io.smallrye.config.SmallRyeConfig; /** * This creates Extensions Page */ public class ConfigurationProcessor { + private static final String QUOTED_DOT = "\".\""; + private static final String QUOTED_DOT_KEY = "$$QUOTED_DOT$$"; + @BuildStep(onlyIf = IsDevelopment.class) InternalPageBuildItem createConfigurationPages(List configDescriptionBuildItems, Optional devServicesLauncherConfig) { @@ -75,7 +88,233 @@ private List getAllConfig(List co item.getAllowedValues(), item.getConfigPhase().name())); } - return configDescriptions; + + Set devServicesConfig = new HashSet<>(); + if (devServicesLauncherConfig.isPresent()) { + devServicesConfig.addAll(devServicesLauncherConfig.get().getConfig().keySet()); + } + + return calculate(configDescriptions, devServicesConfig); + } + + private List calculate(List cd, Set devServicesProperties) { + List configDescriptions = new ArrayList<>(cd); + + List ordered = new ArrayList<>(); + List properties = new ArrayList<>(); + SmallRyeConfig current = (SmallRyeConfig) ConfigProvider.getConfig(); + + Map, Set> allPropertySegments = new HashMap<>(); + Set propertyNames = new HashSet<>(); + current.getPropertyNames().forEach(propertyNames::add); + for (String propertyName : propertyNames) { + propertyName = propertyName.replace(QUOTED_DOT, QUOTED_DOT_KEY); // Make sure dots can be quoted + String[] parts = propertyName.split("\\."); + + List accumulate = new ArrayList<>(); + //we never want to add the full string + //hence -1 + for (int i = 0; i < parts.length - 1; ++i) { + if (parts[i].isEmpty()) { + //this can't map to a quarkus prop as it has an empty segment + //so skip + break; + } + // If there was a quoted dot, put that back + if (parts[i].contains(QUOTED_DOT_KEY)) { + parts[i] = parts[i].replaceAll(QUOTED_DOT_KEY, QUOTED_DOT); + } + + accumulate.add(parts[i]); + //if there is both a quoted and unquoted version we only want to apply the quoted version + //and remove the unquoted one + Set potentialSegmentSet = allPropertySegments.computeIfAbsent(List.copyOf(accumulate), + (k) -> new HashSet<>()); + if (isQuoted(parts[i + 1])) { + potentialSegmentSet.add(parts[i + 1]); + potentialSegmentSet.remove(parts[i + 1].substring(1, parts[i + 1].length() - 1)); + } else { + if (!potentialSegmentSet.contains(ensureQuoted(parts[i + 1]))) { + potentialSegmentSet.add(parts[i + 1]); + } + } + + } + } + + Map, Set> wildcardsToAdd = new HashMap<>(); + Map foundItems = new HashMap<>(); + Set bannedExpansionCombos = new HashSet<>(); + //we iterate over every config description + for (ConfigDescription item : configDescriptions) { + //if they are a non-wildcard description we just add them directly + if (!item.getName().contains("{*}")) { + //we don't want to accidentally use these properties as name expansions + //we ban them which means that the only way the name can be expanded into a map + //is if it is quoted + bannedExpansionCombos.add(item.getName()); + for (int i = 0; i < item.getName().length(); ++i) { + //add all possible segments to the banned list + if (item.getName().charAt(i) == '.') { + bannedExpansionCombos.add(item.getName().substring(0, i)); + } + } + properties.add(item.getName()); + item.setConfigValue(current.getConfigValue(item.getName())); + + String configSourceName = item.getConfigValue().getConfigSourceName(); + item.setConfigSource(configSourceName); + ordered.add(item); + } else if (!item.getName().startsWith("quarkus.log.filter")) { //special case, we use this internally and we don't want it clogging up the editor + //we need to figure out how to expand it + //this can have multiple stars + List> componentParts = new ArrayList<>(); + List accumulator = new ArrayList<>(); + //keys that were used to expand, checked against the banned list before adding + for (var i : item.getName().split("\\.")) { + if (i.equals("{*}")) { + componentParts.add(accumulator); + accumulator = new ArrayList<>(); + } else { + accumulator.add(i); + } + } + //note that accumulator is still holding the final part + //we need it later, but we don't want it in this loop + Map, Set> building = new HashMap<>(); + building.put(List.of(), new HashSet<>()); + for (List currentPart : componentParts) { + Map, Set> newBuilding = new HashMap<>(); + for (Map.Entry, Set> entry : building.entrySet()) { + List attempt = entry.getKey(); + List newBase = new ArrayList<>(attempt); + newBase.addAll(currentPart); + wildcardsToAdd.put(newBase, entry.getValue()); + Set potential = allPropertySegments.get(newBase); + if (potential != null) { + bannedExpansionCombos.add(String.join(".", newBase).replace("\"", "")); + for (String definedName : potential) { + List toAdd = new ArrayList<>(newBase); + toAdd.add(definedName); + //for expansion keys we always use unquoted values, same with banned + //so we are always comparing unquoted + Set expansionKeys = new HashSet<>(entry.getValue()); + expansionKeys.add(String.join(".", newBase) + "." + definedName); + newBuilding.put(toAdd, expansionKeys); + } + } + } + building = newBuilding; + } + //now we have our config properties + for (var entry : building.entrySet()) { + List segments = entry.getKey(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < segments.size(); ++i) { + if (i > 0) { + sb.append("."); + } + sb.append(segments.get(i)); + } + //accumulator holds the find string + for (String s : accumulator) { + sb.append(".").append(s); + } + String expandedName = sb.toString(); + foundItems.put(expandedName, new Holder(entry.getValue(), item)); + } + } + } + for (Map.Entry e : foundItems.entrySet()) { + boolean ok = true; + for (String key : e.getValue().expansionKeys) { + if (bannedExpansionCombos.contains(key)) { + ok = false; + break; + } + } + if (!ok) { + continue; + } + String expandedName = e.getKey(); + var item = e.getValue().configDescription; + ConfigDescription newDesc = new ConfigDescription(expandedName, item.getDescription(), + item.getDefaultValue(), devServicesProperties.contains(expandedName), item.getTypeName(), + item.getAllowedValues(), + item.getConfigPhase()); + + properties.add(newDesc.getName()); + newDesc.setConfigValue(current.getConfigValue(newDesc.getName())); + + String configSourceName = newDesc.getConfigValue().getConfigSourceName(); + int configSourceOrdinal = newDesc.getConfigValue().getConfigSourceOrdinal(); + newDesc.setConfigSource(configSourceName); + ordered.add(newDesc); + } + + //now add our star properties + for (var entry : wildcardsToAdd.entrySet()) { + boolean ok = true; + for (String key : entry.getValue()) { + if (bannedExpansionCombos.contains(key)) { + ok = false; + break; + } + } + if (!ok) { + continue; + } + List segments = entry.getKey(); + StringBuilder sb = new StringBuilder(); + for (String segment : segments) { + sb.append(segment); + sb.append("."); + } + String expandedName = sb.toString(); + ConfigDescription newDesc = new ConfigDescription(expandedName, true); + + properties.add(newDesc.getName()); + newDesc.setConfigValue(current.getConfigValue(newDesc.getName())); + String configSourceName = newDesc.getConfigValue().getConfigSourceName(); + newDesc.setConfigSource(configSourceName); + ordered.add(newDesc); + } + + for (ConfigSource configSource : current.getConfigSources()) { + if (configSource.getName().equals("PropertiesConfigSource[source=Build system]")) { + properties.addAll(configSource.getPropertyNames()); + } + } + + withoutExpansion(() -> { + for (String propertyName : current.getPropertyNames()) { + if (properties.contains(propertyName)) { + continue; + } + + ConfigDescription item = new ConfigDescription(propertyName, null, null, current.getConfigValue(propertyName)); + ConfigValue configValue = current.getConfigValue(propertyName); + ConfigSourceName csn = new ConfigSourceName(configValue.getConfigSourceName(), + configValue.getConfigSourceOrdinal()); + item.setConfigSource(csn.getName()); + ordered.add(item); + + configDescriptions.add(item); + } + }); + + return ordered; + } + + private String ensureQuoted(String part) { + if (isQuoted(part)) { + return part; + } + return "\"" + part + "\""; + } + + private boolean isQuoted(String part) { + return part.length() >= 2 && part.charAt(0) == '\"' && part.charAt(part.length() - 1) == '\"'; } private static final Pattern codePattern = Pattern.compile("(\\{@code )([^}]+)(\\})"); @@ -93,4 +332,14 @@ static String formatJavadoc(String val) { val = val.replace("@deprecated", "
Deprecated"); return val; } + + static class Holder { + final Set expansionKeys; + final ConfigDescription configDescription; + + private Holder(Set expansionKeys, ConfigDescription configDescription) { + this.expansionKeys = expansionKeys; + this.configDescription = configDescription; + } + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfigEditorProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfigEditorProcessor.java index f12ccb4431669..9825d2090f6b2 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfigEditorProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ConfigEditorProcessor.java @@ -8,7 +8,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -118,7 +117,6 @@ protected void handlePost(RoutingContext event, MultiMap form) throws Exception updateConfig(autoconfig); } else if (action.equals("updateProperties")) { - Map properties = new LinkedHashMap<>(); String values = event.request().getFormAttribute("values"); setConfig(values); } diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ConfigDescription.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ConfigDescription.java index 7f772777fd7df..df987fb0e77cc 100644 --- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ConfigDescription.java +++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ConfigDescription.java @@ -14,6 +14,7 @@ public class ConfigDescription implements Comparable { private List allowedValues; private String configPhase; private boolean wildcardEntry = false; + private String configSource; public ConfigDescription() { } @@ -118,6 +119,14 @@ public ConfigDescription setWildcardEntry(boolean wildcardEntry) { return this; } + public String getConfigSource() { + return configSource; + } + + public void setConfigSource(String configSource) { + this.configSource = configSource; + } + @Override public int compareTo(ConfigDescription o) { int ordinal = Integer.compare(o.configValue.getConfigSourceOrdinal(), this.configValue.getConfigSourceOrdinal());