diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigRoot.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigRoot.java index b42451ddd609c..86564da225709 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigRoot.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/discovery/DiscoveryConfigRoot.java @@ -11,14 +11,17 @@ public final class DiscoveryConfigRoot extends DiscoveryRootElement { private final String prefix; + private final String overriddenDocPrefix; private final ConfigPhase phase; private final String overriddenDocFileName; - public DiscoveryConfigRoot(Extension extension, String prefix, String binaryName, String qualifiedName, + public DiscoveryConfigRoot(Extension extension, String prefix, String overriddenDocPrefix, + String binaryName, String qualifiedName, ConfigPhase configPhase, String overriddenDocFileName, boolean configMapping) { super(extension, binaryName, qualifiedName, configMapping); this.prefix = prefix; + this.overriddenDocPrefix = overriddenDocPrefix; this.phase = configPhase; this.overriddenDocFileName = overriddenDocFileName; } @@ -27,6 +30,10 @@ public String getPrefix() { return prefix; } + public String getOverriddenDocPrefix() { + return overriddenDocPrefix; + } + public ConfigPhase getPhase() { return phase; } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/JavadocMerger.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/JavadocMerger.java new file mode 100644 index 0000000000000..43e4f42a36a41 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/JavadocMerger.java @@ -0,0 +1,45 @@ +package io.quarkus.annotation.processor.documentation.config.merger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkus.annotation.processor.Outputs; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements; +import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; +import io.quarkus.annotation.processor.documentation.config.util.JacksonMappers; + +public final class JavadocMerger { + + private JavadocMerger() { + } + + public static JavadocRepository mergeJavadocElements(List buildOutputDirectories) { + Map javadocElementsMap = new HashMap<>(); + + for (Path buildOutputDirectory : buildOutputDirectories) { + Path javadocPath = buildOutputDirectory.resolve(Outputs.QUARKUS_CONFIG_DOC_JAVADOC); + if (!Files.isReadable(javadocPath)) { + continue; + } + + try { + JavadocElements javadocElements = JacksonMappers.yamlObjectReader().readValue(javadocPath.toFile(), + JavadocElements.class); + + if (javadocElements.elements() == null || javadocElements.elements().isEmpty()) { + continue; + } + + javadocElementsMap.putAll(javadocElements.elements()); + } catch (IOException e) { + throw new IllegalStateException("Unable to parse: " + javadocPath, e); + } + } + + return new JavadocRepository(javadocElementsMap); + } +} diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/JavadocRepository.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/JavadocRepository.java similarity index 85% rename from devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/JavadocRepository.java rename to core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/JavadocRepository.java index b4127463455ab..8fcb1840df6b7 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/JavadocRepository.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/JavadocRepository.java @@ -1,4 +1,4 @@ -package io.quarkus.maven.config.doc; +package io.quarkus.annotation.processor.documentation.config.merger; import java.util.Map; import java.util.Optional; @@ -6,7 +6,7 @@ import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; import io.quarkus.annotation.processor.documentation.config.util.Markers; -final class JavadocRepository { +public final class JavadocRepository { private final Map javadocElementsMap; diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/MergedModel.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/MergedModel.java similarity index 84% rename from devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/MergedModel.java rename to core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/MergedModel.java index 11d93d54d8331..de6f13ce7cdf7 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/MergedModel.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/MergedModel.java @@ -1,4 +1,4 @@ -package io.quarkus.maven.config.doc; +package io.quarkus.annotation.processor.documentation.config.merger; import java.util.Collections; import java.util.List; @@ -8,6 +8,9 @@ import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; import io.quarkus.annotation.processor.documentation.config.model.Extension; +/** + * The merged model we obtain after merging all the ResolvedModels from the current project. + */ public class MergedModel { private final Map> configRoots; @@ -16,7 +19,7 @@ public class MergedModel { private final Map> generatedConfigSections; - public MergedModel(Map> configRoots, + MergedModel(Map> configRoots, Map configRootsInSpecificFile, Map> configSections) { this.configRoots = Collections.unmodifiableMap(configRoots); diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/ModelMerger.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/ModelMerger.java new file mode 100644 index 0000000000000..29867c3299fc2 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/merger/ModelMerger.java @@ -0,0 +1,140 @@ +package io.quarkus.annotation.processor.documentation.config.merger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import io.quarkus.annotation.processor.Outputs; +import io.quarkus.annotation.processor.documentation.config.model.AbstractConfigItem; +import io.quarkus.annotation.processor.documentation.config.model.ConfigItemCollection; +import io.quarkus.annotation.processor.documentation.config.model.ConfigRoot; +import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; +import io.quarkus.annotation.processor.documentation.config.model.Extension; +import io.quarkus.annotation.processor.documentation.config.model.Extension.NameSource; +import io.quarkus.annotation.processor.documentation.config.model.ResolvedModel; +import io.quarkus.annotation.processor.documentation.config.util.JacksonMappers; + +public final class ModelMerger { + + private ModelMerger() { + } + + /** + * Merge all the resolved models obtained from a list of build output directories (e.g. in the case of Maven, the list of + * target/ directories found in the parent directory scanned). + */ + public static MergedModel mergeModel(List buildOutputDirectories) { + // keyed on extension and then top level prefix + Map> configRoots = new HashMap<>(); + // keyed on file name + Map configRootsInSpecificFile = new TreeMap<>(); + // keyed on extension + Map> generatedConfigSections = new TreeMap<>(); + + for (Path buildOutputDirectory : buildOutputDirectories) { + Path resolvedModelPath = buildOutputDirectory.resolve(Outputs.QUARKUS_CONFIG_DOC_MODEL); + if (!Files.isReadable(resolvedModelPath)) { + continue; + } + + try { + ResolvedModel resolvedModel = JacksonMappers.yamlObjectReader().readValue(resolvedModelPath.toFile(), + ResolvedModel.class); + + if (resolvedModel.getConfigRoots() == null || resolvedModel.getConfigRoots().isEmpty()) { + continue; + } + + for (ConfigRoot configRoot : resolvedModel.getConfigRoots()) { + if (configRoot.getOverriddenDocFileName() != null) { + ConfigRoot existingConfigRootInSpecificFile = configRootsInSpecificFile + .get(configRoot.getOverriddenDocFileName()); + + if (existingConfigRootInSpecificFile == null) { + configRootsInSpecificFile.put(configRoot.getOverriddenDocFileName(), configRoot); + } else { + if (!existingConfigRootInSpecificFile.getExtension().equals(configRoot.getExtension()) + || !existingConfigRootInSpecificFile.getPrefix().equals(configRoot.getPrefix())) { + throw new IllegalStateException( + "Two config roots with different extensions or prefixes cannot be merged in the same specific config file: " + + configRoot.getOverriddenDocFileName()); + } + + existingConfigRootInSpecificFile.merge(configRoot); + } + + continue; + } + + Map extensionConfigRoots = configRoots.computeIfAbsent(configRoot.getExtension(), + e -> new HashMap<>()); + + ConfigRoot existingConfigRoot = extensionConfigRoots.get(configRoot.getTopLevelPrefix()); + + if (existingConfigRoot == null) { + extensionConfigRoots.put(configRoot.getTopLevelPrefix(), configRoot); + } else { + existingConfigRoot.merge(configRoot); + } + } + } catch (IOException e) { + throw new IllegalStateException("Unable to parse: " + resolvedModelPath, e); + } + } + + configRoots = retainBestExtensionKey(configRoots); + + for (Entry> extensionConfigRootsEntry : configRoots.entrySet()) { + List extensionGeneratedConfigSections = generatedConfigSections + .computeIfAbsent(extensionConfigRootsEntry.getKey(), e -> new ArrayList<>()); + + for (ConfigRoot configRoot : extensionConfigRootsEntry.getValue().values()) { + collectGeneratedConfigSections(extensionGeneratedConfigSections, configRoot); + } + } + + return new MergedModel(configRoots, configRootsInSpecificFile, generatedConfigSections); + } + + private static Map> retainBestExtensionKey( + Map> configRoots) { + return configRoots.entrySet().stream().collect(Collectors.toMap(e -> { + Extension extension = e.getKey(); + + for (ConfigRoot configRoot : e.getValue().values()) { + if (configRoot.getExtension().nameSource().isBetterThan(extension.nameSource())) { + extension = configRoot.getExtension(); + } + if (NameSource.EXTENSION_METADATA.equals(extension.nameSource())) { + // we won't find any better + break; + } + } + + return extension; + }, e -> e.getValue(), (k1, k2) -> k1, TreeMap::new)); + } + + private static void collectGeneratedConfigSections(List extensionGeneratedConfigSections, + ConfigItemCollection configItemCollection) { + for (AbstractConfigItem configItem : configItemCollection.getItems()) { + if (!configItem.isSection()) { + continue; + } + + ConfigSection configSection = (ConfigSection) configItem; + if (configSection.isGenerated()) { + extensionGeneratedConfigSections.add(configSection); + } + + collectGeneratedConfigSections(extensionGeneratedConfigSections, configSection); + } + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigRoot.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigRoot.java index 583e081199d3e..756b226d3937b 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigRoot.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigRoot.java @@ -7,6 +7,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; + +import io.quarkus.annotation.processor.documentation.config.util.Markers; /** * At this stage, a config root is actually a prefix: we merged all the config roots with the same prefix. @@ -17,14 +20,19 @@ public class ConfigRoot implements ConfigItemCollection { private final Extension extension; private final String prefix; + // used by the doc generation to classify config roots + private final String topLevelPrefix; - private String overriddenDocFileName; + private final String overriddenDocFileName; private final List items = new ArrayList<>(); private final Set qualifiedNames = new HashSet<>(); - public ConfigRoot(Extension extension, String prefix) { + public ConfigRoot(Extension extension, String prefix, String overriddenDocPrefix, String overriddenDocFileName) { this.extension = extension; this.prefix = prefix; + this.overriddenDocFileName = overriddenDocFileName; + this.topLevelPrefix = overriddenDocPrefix != null ? buildTopLevelPrefix(overriddenDocPrefix) + : buildTopLevelPrefix(prefix); } public Extension getExtension() { @@ -39,13 +47,6 @@ public String getOverriddenDocFileName() { return overriddenDocFileName; } - public void setOverriddenDocFileName(String overriddenDocFileName) { - if (this.overriddenDocFileName != null) { - return; - } - this.overriddenDocFileName = overriddenDocFileName; - } - public void addQualifiedName(String qualifiedName) { qualifiedNames.add(qualifiedName); } @@ -64,6 +65,10 @@ public List getItems() { return Collections.unmodifiableList(items); } + public String getTopLevelPrefix() { + return topLevelPrefix; + } + public void merge(ConfigRoot other) { this.qualifiedNames.addAll(other.getQualifiedNames()); @@ -116,4 +121,14 @@ public boolean hasMemorySizeType() { } return false; } + + private static String buildTopLevelPrefix(String prefix) { + String[] prefixSegments = prefix.split(Pattern.quote(Markers.DOT)); + + if (prefixSegments.length == 1) { + return prefixSegments[0]; + } + + return prefixSegments[0] + Markers.DOT + prefixSegments[1]; + } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigSection.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigSection.java index 5f008988c0361..f816dc8f08f6e 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigSection.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/model/ConfigSection.java @@ -9,11 +9,13 @@ public final class ConfigSection extends AbstractConfigItem implements ConfigIte private boolean generated; private final List items = new ArrayList<>(); + private final int level; - public ConfigSection(String sourceClass, String sourceName, String path, String type, boolean generated, - boolean deprecated) { + public ConfigSection(String sourceClass, String sourceName, String path, String type, int level, + boolean generated, boolean deprecated) { super(sourceClass, sourceName, path, type, deprecated); this.generated = generated; + this.level = level; } @Override @@ -43,6 +45,10 @@ public boolean isGenerated() { return generated; } + public int getLevel() { + return level; + } + /** * This is used when we merge ConfigSection at the ConfigRoot level. * It can happen when for instance a path is both used at a given level and in an unnamed map. diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/resolver/ConfigResolver.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/resolver/ConfigResolver.java index 772c60ddad671..88a4098ca0e50 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/resolver/ConfigResolver.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/resolver/ConfigResolver.java @@ -67,14 +67,14 @@ public ResolvedModel resolveModel() { List configRoots = new ArrayList<>(); for (DiscoveryConfigRoot discoveryConfigRoot : configCollector.getConfigRoots()) { - ConfigRoot configRoot = new ConfigRoot(discoveryConfigRoot.getExtension(), discoveryConfigRoot.getPrefix()); + ConfigRoot configRoot = new ConfigRoot(discoveryConfigRoot.getExtension(), discoveryConfigRoot.getPrefix(), + discoveryConfigRoot.getOverriddenDocPrefix(), discoveryConfigRoot.getOverriddenDocFileName()); Map existingRootConfigSections = new HashMap<>(); - configRoot.setOverriddenDocFileName(discoveryConfigRoot.getOverriddenDocFileName()); configRoot.addQualifiedName(discoveryConfigRoot.getQualifiedName()); ResolutionContext context = new ResolutionContext(configRoot.getPrefix(), new ArrayList<>(), discoveryConfigRoot, - configRoot, false, false, false); + configRoot, 0, false, false, false); for (DiscoveryConfigProperty discoveryConfigProperty : discoveryConfigRoot.getProperties().values()) { resolveProperty(configRoot, existingRootConfigSections, discoveryConfigRoot.getPhase(), context, discoveryConfigProperty); @@ -132,16 +132,17 @@ private void resolveProperty(ConfigRoot configRoot, Map e } else { configSection = new ConfigSection(discoveryConfigProperty.getSourceClass(), discoveryConfigProperty.getSourceName(), propertyPath, typeQualifiedName, - discoveryConfigProperty.isSectionGenerated(), deprecated); + context.getSectionLevel(), discoveryConfigProperty.isSectionGenerated(), deprecated); context.getItemCollection().addItem(configSection); existingRootConfigSections.put(propertyPath, configSection); } configGroupContext = new ResolutionContext(potentiallyMappedPath, additionalPaths, discoveryConfigGroup, - configSection, isWithinMap, isWithMapWithUnnamedKey, deprecated); + configSection, context.getSectionLevel() + 1, isWithinMap, isWithMapWithUnnamedKey, deprecated); } else { configGroupContext = new ResolutionContext(potentiallyMappedPath, additionalPaths, discoveryConfigGroup, - context.getItemCollection(), isWithinMap, isWithMapWithUnnamedKey, deprecated); + context.getItemCollection(), context.getSectionLevel(), isWithinMap, isWithMapWithUnnamedKey, + deprecated); } for (DiscoveryConfigProperty configGroupProperty : discoveryConfigGroup.getProperties().values()) { @@ -244,13 +245,14 @@ private static class ResolutionContext { private final List additionalPaths; private final DiscoveryRootElement discoveryRootElement; private final ConfigItemCollection itemCollection; + private final int sectionLevel; private final boolean withinMap; private final boolean withinMapWithUnnamedKey; private final boolean deprecated; private ResolutionContext(String path, List additionalPaths, DiscoveryRootElement discoveryRootElement, ConfigItemCollection itemCollection, - boolean withinMap, boolean withinMapWithUnnamedKey, boolean deprecated) { + int sectionLevel, boolean withinMap, boolean withinMapWithUnnamedKey, boolean deprecated) { this.path = path; this.additionalPaths = additionalPaths; this.discoveryRootElement = discoveryRootElement; @@ -258,6 +260,7 @@ private ResolutionContext(String path, List additionalPaths, DiscoveryRo this.withinMap = withinMap; this.withinMapWithUnnamedKey = withinMapWithUnnamedKey; this.deprecated = deprecated; + this.sectionLevel = sectionLevel; } public String getPath() { @@ -276,6 +279,10 @@ public ConfigItemCollection getItemCollection() { return itemCollection; } + public int getSectionLevel() { + return sectionLevel; + } + public boolean isWithinMap() { return withinMap; } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigAnnotationScanner.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigAnnotationScanner.java index 7f0aa469e9364..7618489e6326d 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigAnnotationScanner.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigAnnotationScanner.java @@ -75,7 +75,7 @@ public ConfigAnnotationScanner(Config config, Utils utils) { configMappingWithoutConfigRootListeners.add(new JavadocConfigMappingListener(config, utils, configCollector)); } else { configRootListeners.add(new JavadocLegacyConfigRootListener(config, utils, configCollector)); - configRootListeners.add(new LegacyConfigListener(config, utils, configCollector)); + configRootListeners.add(new LegacyConfigRootListener(config, utils, configCollector)); } } else { // TODO #42114 remove once fixed @@ -84,7 +84,7 @@ public ConfigAnnotationScanner(Config config, Utils utils) { configRootListeners.add(new JavadocConfigMappingListener(config, utils, configCollector)); configRootListeners.add(new JavadocLegacyConfigRootListener(config, utils, configCollector)); configRootListeners.add(new ConfigMappingListener(config, utils, configCollector)); - configRootListeners.add(new LegacyConfigListener(config, utils, configCollector)); + configRootListeners.add(new LegacyConfigRootListener(config, utils, configCollector)); configMappingWithoutConfigRootListeners.add(new JavadocConfigMappingListener(config, utils, configCollector)); } @@ -163,7 +163,7 @@ public void scanConfigMappingsWithoutConfigRoot(RoundEnvironment roundEnv, TypeE try { // we need to forge a dummy DiscoveryConfigRoot // it's mostly ignored in the listeners, except for checking if it's a config mapping (for mixed modules) - DiscoveryConfigRoot discoveryConfigRoot = new DiscoveryConfigRoot(config.getExtension(), "dummy", + DiscoveryConfigRoot discoveryConfigRoot = new DiscoveryConfigRoot(config.getExtension(), "dummy", "dummy", utils.element().getBinaryName(configMappingWithoutConfigRoot), configMappingWithoutConfigRoot.getQualifiedName().toString(), ConfigPhase.BUILD_TIME, null, true); diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigMappingListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigMappingListener.java index 3fb6b2d777c96..b790c716a245b 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigMappingListener.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/ConfigMappingListener.java @@ -38,6 +38,7 @@ public Optional onConfigRoot(TypeElement configRoot) { AnnotationMirror configRootAnnotation = null; AnnotationMirror configMappingAnnotion = null; + AnnotationMirror configDocPrefixAnnotation = null; AnnotationMirror configDocFileNameAnnotation = null; for (AnnotationMirror annotationMirror : configRoot.getAnnotationMirrors()) { @@ -51,6 +52,10 @@ public Optional onConfigRoot(TypeElement configRoot) { configMappingAnnotion = annotationMirror; continue; } + if (annotationName.equals(Types.ANNOTATION_CONFIG_DOC_PREFIX)) { + configDocPrefixAnnotation = annotationMirror; + continue; + } if (annotationName.equals(Types.ANNOTATION_CONFIG_DOC_FILE_NAME)) { configDocFileNameAnnotation = annotationMirror; continue; @@ -76,6 +81,18 @@ public Optional onConfigRoot(TypeElement configRoot) { } } + String overriddenDocPrefix = null; + if (configDocPrefixAnnotation != null) { + for (Map.Entry entry : configDocPrefixAnnotation + .getElementValues() + .entrySet()) { + if ("value()".equals(entry.getKey().toString())) { + overriddenDocPrefix = entry.getValue().getValue().toString(); + break; + } + } + } + String overriddenDocFileName = null; if (configDocFileNameAnnotation != null) { for (Map.Entry entry : configDocFileNameAnnotation @@ -91,7 +108,8 @@ public Optional onConfigRoot(TypeElement configRoot) { String rootPrefix = ConfigNamingUtil.getRootPrefix(prefix, "", configRoot.getSimpleName().toString(), configPhase); String binaryName = utils.element().getBinaryName(configRoot); - DiscoveryConfigRoot discoveryConfigRoot = new DiscoveryConfigRoot(config.getExtension(), rootPrefix, + DiscoveryConfigRoot discoveryConfigRoot = new DiscoveryConfigRoot(config.getExtension(), + rootPrefix, overriddenDocPrefix, binaryName, configRoot.getQualifiedName().toString(), configPhase, overriddenDocFileName, true); configCollector.addConfigRoot(discoveryConfigRoot); return Optional.of(discoveryConfigRoot); diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java index 8c229e40a494a..385fbf401fbe1 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocConfigMappingListener.java @@ -7,6 +7,7 @@ import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryRootElement; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; import io.quarkus.annotation.processor.documentation.config.formatter.JavadocToAsciidocTransformer; import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; @@ -38,20 +39,30 @@ public void onEnclosedMethod(DiscoveryRootElement discoveryRootElement, TypeElem } Optional rawJavadoc = utils.element().getJavadoc(method); + boolean isSection = utils.element().isAnnotationPresent(method, Types.ANNOTATION_CONFIG_DOC_SECTION); if (rawJavadoc.isEmpty()) { // We require a Javadoc for config items that are not config groups except if they are a section - if (!resolvedType.isConfigGroup() - || utils.element().isAnnotationPresent(method, Types.ANNOTATION_CONFIG_DOC_SECTION)) { + if (!resolvedType.isConfigGroup() || isSection) { utils.element().addMissingJavadocError(method); } return; } - ParsedJavadoc parsedJavadoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(rawJavadoc.get()); + if (isSection) { + // for sections, we only keep the title + ParsedJavadocSection parsedJavadocSection = JavadocToAsciidocTransformer.INSTANCE + .parseConfigSectionJavadoc(rawJavadoc.get()); - configCollector.addJavadocElement( - clazz.getQualifiedName().toString() + Markers.DOT + method.getSimpleName().toString(), - new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), rawJavadoc.get())); + configCollector.addJavadocElement( + clazz.getQualifiedName().toString() + Markers.DOT + method.getSimpleName().toString(), + new JavadocElement(parsedJavadocSection.title(), null, rawJavadoc.get())); + } else { + ParsedJavadoc parsedJavadoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(rawJavadoc.get()); + + configCollector.addJavadocElement( + clazz.getQualifiedName().toString() + Markers.DOT + method.getSimpleName().toString(), + new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), rawJavadoc.get())); + } } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java index 934c4c7639729..8b08f37a6fac4 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/JavadocLegacyConfigRootListener.java @@ -7,6 +7,7 @@ import io.quarkus.annotation.processor.documentation.config.discovery.DiscoveryRootElement; import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadoc; +import io.quarkus.annotation.processor.documentation.config.discovery.ParsedJavadocSection; import io.quarkus.annotation.processor.documentation.config.discovery.ResolvedType; import io.quarkus.annotation.processor.documentation.config.formatter.JavadocToAsciidocTransformer; import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; @@ -38,20 +39,30 @@ public void onEnclosedField(DiscoveryRootElement discoveryRootElement, TypeEleme } Optional rawJavadoc = utils.element().getJavadoc(field); + boolean isSection = utils.element().isAnnotationPresent(field, Types.ANNOTATION_CONFIG_DOC_SECTION); if (rawJavadoc.isEmpty()) { // We require a Javadoc for config items that are not config groups except if they are a section - if (!resolvedType.isConfigGroup() - || utils.element().isAnnotationPresent(field, Types.ANNOTATION_CONFIG_DOC_SECTION)) { + if (!resolvedType.isConfigGroup() || isSection) { utils.element().addMissingJavadocError(field); } return; } - ParsedJavadoc parsedJavadoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(rawJavadoc.get()); + if (isSection) { + // for sections, we only keep the title + ParsedJavadocSection parsedJavadocSection = JavadocToAsciidocTransformer.INSTANCE + .parseConfigSectionJavadoc(rawJavadoc.get()); - configCollector.addJavadocElement( - clazz.getQualifiedName().toString() + Markers.DOT + field.getSimpleName().toString(), - new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), rawJavadoc.get())); + configCollector.addJavadocElement( + clazz.getQualifiedName().toString() + Markers.DOT + field.getSimpleName().toString(), + new JavadocElement(parsedJavadocSection.title(), null, rawJavadoc.get())); + } else { + ParsedJavadoc parsedJavadoc = JavadocToAsciidocTransformer.INSTANCE.parseConfigItemJavadoc(rawJavadoc.get()); + + configCollector.addJavadocElement( + clazz.getQualifiedName().toString() + Markers.DOT + field.getSimpleName().toString(), + new JavadocElement(parsedJavadoc.description(), parsedJavadoc.since(), rawJavadoc.get())); + } } } diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/LegacyConfigListener.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/LegacyConfigRootListener.java similarity index 89% rename from core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/LegacyConfigListener.java rename to core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/LegacyConfigRootListener.java index 8d9d3d823c85e..e4b3e31a84030 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/LegacyConfigListener.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/scanner/LegacyConfigRootListener.java @@ -23,9 +23,9 @@ import io.quarkus.annotation.processor.util.Strings; import io.quarkus.annotation.processor.util.Utils; -public class LegacyConfigListener extends AbstractConfigListener { +public class LegacyConfigRootListener extends AbstractConfigListener { - LegacyConfigListener(Config config, Utils utils, ConfigCollector configCollector) { + LegacyConfigRootListener(Config config, Utils utils, ConfigCollector configCollector) { super(config, utils, configCollector); } @@ -39,6 +39,7 @@ public Optional onConfigRoot(TypeElement configRoot) { ConfigPhase configPhase = ConfigPhase.BUILD_TIME; AnnotationMirror configRootAnnotation = null; + AnnotationMirror configDocPrefixAnnotation = null; AnnotationMirror configDocFileNameAnnotation = null; for (AnnotationMirror annotationMirror : configRoot.getAnnotationMirrors()) { @@ -48,6 +49,10 @@ public Optional onConfigRoot(TypeElement configRoot) { configRootAnnotation = annotationMirror; continue; } + if (annotationName.equals(Types.ANNOTATION_CONFIG_DOC_PREFIX)) { + configDocPrefixAnnotation = annotationMirror; + continue; + } if (annotationName.equals(Types.ANNOTATION_CONFIG_DOC_FILE_NAME)) { configDocFileNameAnnotation = annotationMirror; continue; @@ -73,6 +78,18 @@ public Optional onConfigRoot(TypeElement configRoot) { } } + String overriddenDocPrefix = null; + if (configDocPrefixAnnotation != null) { + for (Map.Entry entry : configDocPrefixAnnotation + .getElementValues() + .entrySet()) { + if ("value()".equals(entry.getKey().toString())) { + overriddenDocPrefix = entry.getValue().getValue().toString(); + break; + } + } + } + String overriddenDocFileName = null; if (configDocFileNameAnnotation != null) { for (Map.Entry entry : configDocFileNameAnnotation @@ -88,7 +105,8 @@ public Optional onConfigRoot(TypeElement configRoot) { String rootPrefix = ConfigNamingUtil.getRootPrefix(prefix, name, configRoot.getSimpleName().toString(), configPhase); String binaryName = utils.element().getBinaryName(configRoot); - DiscoveryConfigRoot discoveryConfigRoot = new DiscoveryConfigRoot(config.getExtension(), rootPrefix, + DiscoveryConfigRoot discoveryConfigRoot = new DiscoveryConfigRoot(config.getExtension(), + rootPrefix, overriddenDocPrefix, binaryName, configRoot.getQualifiedName().toString(), configPhase, overriddenDocFileName, false); configCollector.addConfigRoot(discoveryConfigRoot); diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JacksonMappers.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JacksonMappers.java new file mode 100644 index 0000000000000..525da1c1788b6 --- /dev/null +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JacksonMappers.java @@ -0,0 +1,33 @@ +package io.quarkus.annotation.processor.documentation.config.util; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; + +public final class JacksonMappers { + + private static final ObjectWriter JSON_OBJECT_WRITER = new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_DEFAULT).writerWithDefaultPrettyPrinter(); + private static final ObjectWriter YAML_OBJECT_WRITER = new ObjectMapper(new YAMLFactory()) + .setSerializationInclusion(JsonInclude.Include.NON_DEFAULT).writer(); + private static final ObjectReader YAML_OBJECT_READER = new ObjectMapper(new YAMLFactory()) + .registerModule(new ParameterNamesModule()).reader(); + + private JacksonMappers() { + } + + public static ObjectWriter jsonObjectWriter() { + return JSON_OBJECT_WRITER; + } + + public static ObjectWriter yamlObjectWriter() { + return YAML_OBJECT_WRITER; + } + + public static ObjectReader yamlObjectReader() { + return YAML_OBJECT_READER; + } +} diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java index c9970cefcf8d3..0e4115d278b86 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/JavadocUtil.java @@ -6,8 +6,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.annotation.processing.ProcessingEnvironment; - public final class JavadocUtil { static final String VERTX_JAVA_DOC_SITE = "https://vertx.io/docs/apidocs/"; @@ -24,7 +22,7 @@ public final class JavadocUtil { EXTENSION_JAVA_DOC_LINK.put("io.agroal.", AGROAL_API_JAVA_DOC_SITE); } - private JavadocUtil(ProcessingEnvironment processingEnv) { + private JavadocUtil() { } /** diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/Types.java b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/Types.java index d8016274dd929..f98d74f701153 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/Types.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/documentation/config/util/Types.java @@ -30,6 +30,7 @@ private Types() { public static final String ANNOTATION_CONFIG_DOC_ENUM_VALUE = "io.quarkus.runtime.annotations.ConfigDocEnumValue"; public static final String ANNOTATION_CONFIG_DOC_DEFAULT = "io.quarkus.runtime.annotations.ConfigDocDefault"; public static final String ANNOTATION_CONFIG_DOC_FILE_NAME = "io.quarkus.runtime.annotations.ConfigDocFilename"; + public static final String ANNOTATION_CONFIG_DOC_PREFIX = "io.quarkus.runtime.annotations.ConfigDocPrefix"; public static final String ANNOTATION_CONFIG_WITH_CONVERTER = "io.smallrye.config.WithConverter"; public static final String ANNOTATION_CONFIG_WITH_NAME = "io.smallrye.config.WithName"; diff --git a/core/processor/src/main/java/io/quarkus/annotation/processor/util/FilerUtil.java b/core/processor/src/main/java/io/quarkus/annotation/processor/util/FilerUtil.java index 8875b96c7abb1..fa8765cdc7798 100644 --- a/core/processor/src/main/java/io/quarkus/annotation/processor/util/FilerUtil.java +++ b/core/processor/src/main/java/io/quarkus/annotation/processor/util/FilerUtil.java @@ -21,28 +21,11 @@ import javax.tools.FileObject; import javax.tools.StandardLocation; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; - +import io.quarkus.annotation.processor.documentation.config.util.JacksonMappers; import io.quarkus.bootstrap.util.PropertyUtils; public class FilerUtil { - private static final ObjectWriter JSON_OBJECT_WRITER; - private static final ObjectMapper YAML_OBJECT_MAPPER = new ObjectMapper(new YAMLFactory()); - - static { - } - - static { - ObjectMapper jsonObjectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); - JSON_OBJECT_WRITER = jsonObjectMapper.writerWithDefaultPrettyPrinter(); - - YAML_OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT); - } - private final ProcessingEnvironment processingEnv; FilerUtil(ProcessingEnvironment processingEnv) { @@ -109,7 +92,7 @@ public void writeJson(String filePath, Object value) { filePath.toString()); try (OutputStream os = jsonResource.openOutputStream()) { - JSON_OBJECT_WRITER.writeValue(os, value); + JacksonMappers.jsonObjectWriter().writeValue(os, value); } } catch (IOException e) { processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write " + filePath + ": " + e); @@ -126,7 +109,7 @@ public Path writeModel(Path filePath, Object value) { Path yamlModelPath = getTargetPath().resolve(filePath); try { Files.createDirectories(yamlModelPath.getParent()); - YAML_OBJECT_MAPPER.writeValue(yamlModelPath.toFile(), value); + JacksonMappers.yamlObjectWriter().writeValue(yamlModelPath.toFile(), value); return yamlModelPath; } catch (IOException e) { @@ -171,7 +154,7 @@ public Optional> getExtensionMetadata() { try (InputStream is = fileObject.openInputStream()) { String yamlMetadata = new String(is.readAllBytes(), StandardCharsets.UTF_8); - Map extensionMetadata = YAML_OBJECT_MAPPER.readValue(yamlMetadata, Map.class); + Map extensionMetadata = JacksonMappers.yamlObjectReader().readValue(yamlMetadata, Map.class); return Optional.of(extensionMetadata); } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ConfigConfig.java b/core/runtime/src/main/java/io/quarkus/runtime/ConfigConfig.java index 075f0b6dc70fe..760c2cabc68c5 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ConfigConfig.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ConfigConfig.java @@ -4,7 +4,7 @@ import java.util.List; import java.util.Optional; -import io.quarkus.runtime.annotations.ConfigDocFilename; +import io.quarkus.runtime.annotations.ConfigDocPrefix; import io.quarkus.runtime.annotations.ConfigPhase; import io.quarkus.runtime.annotations.ConfigRoot; import io.smallrye.config.ConfigMapping; @@ -19,7 +19,7 @@ */ @ConfigMapping(prefix = "quarkus") @ConfigRoot(phase = ConfigPhase.RUN_TIME) -@ConfigDocFilename("quarkus-core_quarkus-config.adoc") +@ConfigDocPrefix("quarkus.config") public interface ConfigConfig { /** * A comma separated list of profiles that will be active when Quarkus launches. diff --git a/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocPrefix.java b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocPrefix.java new file mode 100644 index 0000000000000..befc4bc97c211 --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/annotations/ConfigDocPrefix.java @@ -0,0 +1,24 @@ +package io.quarkus.runtime.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * This annotation can be used when you want to override the top level prefix from the ConfigRoot/ConfigMapping for doc + * generation. + *

+ * This is for instance useful for {@code ConfigConfig}, which is an odd beast. + *

+ * Should be considered very last resort. + */ +@Documented +@Retention(RUNTIME) +@Target({ ElementType.TYPE }) +public @interface ConfigDocPrefix { + + String value(); +} diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/AsciidocFormatter.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/AsciidocFormatter.java index cd347cdf5f07e..211e27875de24 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/AsciidocFormatter.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/AsciidocFormatter.java @@ -5,8 +5,10 @@ import java.util.Optional; import java.util.stream.Collectors; +import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; +import io.quarkus.annotation.processor.documentation.config.model.Extension; import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; import io.quarkus.annotation.processor.documentation.config.util.Types; @@ -16,9 +18,11 @@ final class AsciidocFormatter { private static final String MORE_INFO_ABOUT_TYPE_FORMAT = "link:#%s[icon:question-circle[title=More information about the %s format]]"; private final JavadocRepository javadocRepository; + private final boolean enableEnumTooltips; - AsciidocFormatter(JavadocRepository javadocRepository) { + AsciidocFormatter(JavadocRepository javadocRepository, boolean enableEnumTooltips) { this.javadocRepository = javadocRepository; + this.enableEnumTooltips = enableEnumTooltips; } String formatDescription(ConfigProperty configProperty) { @@ -40,7 +44,7 @@ String formatDescription(ConfigProperty configProperty) { String formatTypeDescription(ConfigProperty configProperty) { String typeContent = ""; - if (configProperty.isEnum()) { + if (configProperty.isEnum() && enableEnumTooltips) { typeContent = configProperty.getEnumAcceptedValues().values().entrySet().stream() .map(e -> { Optional javadocElement = javadocRepository.getElement(configProperty.getType(), @@ -77,7 +81,11 @@ String formatTypeDescription(ConfigProperty configProperty) { String formatDefaultValue(ConfigProperty configProperty) { String defaultValue = configProperty.getDefaultValue(); - if (configProperty.isEnum()) { + if (defaultValue == null) { + return null; + } + + if (configProperty.isEnum() && enableEnumTooltips) { Optional enumConstant = configProperty.getEnumAcceptedValues().values().entrySet().stream() .filter(e -> e.getValue().configValue().equals(defaultValue)) .map(e -> e.getKey()) @@ -185,6 +193,14 @@ String formatSectionTitle(ConfigSection configSection) { return javadoc.substring(0, dotIndex); } + String formatName(Extension extension) { + if (extension.name() == null) { + return extension.artifactId(); + } + + return extension.name(); + } + /** * Note that this is extremely brittle. Apparently, colons breaks the tooltips but if escaped with \, the \ appears in the * output. diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/ConfigSectionJavadoc.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/ConfigSectionJavadoc.java deleted file mode 100644 index c7a040950a80f..0000000000000 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/ConfigSectionJavadoc.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.quarkus.maven.config.doc; - -record ConfigSectionJavadoc(String title, String details) { - - static ConfigSectionJavadoc of(String javadoc) { - if (javadoc == null || javadoc.isBlank()) { - return new ConfigSectionJavadoc(null, null); - } - - javadoc = javadoc.trim(); - int dotIndex = javadoc.indexOf("."); - - if (dotIndex == -1 || dotIndex == javadoc.length() - 1) { - return new ConfigSectionJavadoc(javadoc, null); - } - - return new ConfigSectionJavadoc(javadoc.substring(0, dotIndex).trim(), javadoc.substring(dotIndex).trim()); - } -} diff --git a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java index 66a3b538878df..bd9c3c74aadba 100644 --- a/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java +++ b/devtools/config-doc-maven-plugin/src/main/java/io/quarkus/maven/config/doc/GenerateAsciidocMojo.java @@ -13,13 +13,9 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.regex.Pattern; -import java.util.stream.Collectors; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; @@ -29,22 +25,15 @@ import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; -import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; - -import io.quarkus.annotation.processor.Outputs; -import io.quarkus.annotation.processor.documentation.config.model.AbstractConfigItem; +import io.quarkus.annotation.processor.documentation.config.merger.JavadocMerger; +import io.quarkus.annotation.processor.documentation.config.merger.JavadocRepository; +import io.quarkus.annotation.processor.documentation.config.merger.MergedModel; +import io.quarkus.annotation.processor.documentation.config.merger.ModelMerger; import io.quarkus.annotation.processor.documentation.config.model.ConfigItemCollection; import io.quarkus.annotation.processor.documentation.config.model.ConfigProperty; import io.quarkus.annotation.processor.documentation.config.model.ConfigRoot; import io.quarkus.annotation.processor.documentation.config.model.ConfigSection; import io.quarkus.annotation.processor.documentation.config.model.Extension; -import io.quarkus.annotation.processor.documentation.config.model.Extension.NameSource; -import io.quarkus.annotation.processor.documentation.config.model.JavadocElements; -import io.quarkus.annotation.processor.documentation.config.model.JavadocElements.JavadocElement; -import io.quarkus.annotation.processor.documentation.config.model.ResolvedModel; -import io.quarkus.annotation.processor.documentation.config.util.Markers; import io.quarkus.qute.Engine; import io.quarkus.qute.ReflectionValueResolver; import io.quarkus.qute.UserTagSectionHelper; @@ -53,8 +42,6 @@ @Mojo(name = "generate-asciidoc", defaultPhase = LifecyclePhase.GENERATE_RESOURCES, threadSafe = true) public class GenerateAsciidocMojo extends AbstractMojo { - private static final ObjectMapper YAML_OBJECT_MAPPER = new ObjectMapper(new YAMLFactory()) - .registerModule(new ParameterNamesModule()); private static final String TARGET = "target"; private static final String ADOC_SUFFIX = ".adoc"; @@ -74,6 +61,9 @@ public class GenerateAsciidocMojo extends AbstractMojo { @Parameter(defaultValue = "false") private boolean generateAllConfig; + @Parameter(defaultValue = "false") + private boolean enableEnumTooltips; + @Parameter(defaultValue = "false") private boolean skip; @@ -91,10 +81,10 @@ public void execute() throws MojoExecutionException, MojoFailureException { List targetDirectories = findTargetDirectories(resolvedScanDirectory); - JavadocRepository javadocRepository = findJavadocElements(targetDirectories); - MergedModel mergedModel = mergeModel(targetDirectories); + JavadocRepository javadocRepository = JavadocMerger.mergeJavadocElements(targetDirectories); + MergedModel mergedModel = ModelMerger.mergeModel(targetDirectories); - AsciidocFormatter asciidocFormatter = new AsciidocFormatter(javadocRepository); + AsciidocFormatter asciidocFormatter = new AsciidocFormatter(javadocRepository, enableEnumTooltips); Engine quteEngine = initializeQuteEngine(asciidocFormatter); // we generate a file per extension + top level prefix @@ -114,7 +104,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { try { Files.writeString(configRootAdocPath, - generateConfigReference(quteEngine, summaryTableId, extension, configRoot, true)); + generateConfigReference(quteEngine, summaryTableId, extension, configRoot, "", true)); } catch (Exception e) { throw new MojoExecutionException("Unable to render config roots for top level prefix: " + topLevelPrefix + " in extension: " + extension, e); @@ -150,7 +140,7 @@ public void execute() throws MojoExecutionException, MojoFailureException { try { Files.writeString(configRootAdocPath, - generateConfigReference(quteEngine, summaryTableId, extension, configRoot, true)); + generateConfigReference(quteEngine, summaryTableId, extension, configRoot, "", true)); } catch (Exception e) { throw new MojoExecutionException("Unable to render config roots for specific file: " + fileName + " in extension: " + extension, e); @@ -170,7 +160,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { try { Files.writeString(configSectionAdocPath, - generateConfigReference(quteEngine, summaryTableId, extension, generatedConfigSection, false)); + generateConfigReference(quteEngine, summaryTableId, extension, generatedConfigSection, + "_" + generatedConfigSection.getPath(), false)); } catch (Exception e) { throw new MojoExecutionException( "Unable to render config section for section: " + generatedConfigSection.getPath() @@ -193,12 +184,13 @@ public void execute() throws MojoExecutionException, MojoFailureException { } private static String generateConfigReference(Engine quteEngine, String summaryTableId, Extension extension, - ConfigItemCollection configItemCollection, boolean searchable) { + ConfigItemCollection configItemCollection, String additionalAnchorPrefix, boolean searchable) { return quteEngine.getTemplate("configReference.qute.adoc") .data("extension", extension) .data("configItemCollection", configItemCollection) .data("searchable", searchable) .data("summaryTableId", summaryTableId) + .data("additionalAnchorPrefix", additionalAnchorPrefix) .data("includeDurationNote", configItemCollection.hasDurationType()) .data("includeMemorySizeNote", configItemCollection.hasMemorySizeType()) .render(); @@ -210,6 +202,7 @@ private static String generateAllConfig(Engine quteEngine, .data("configRootsByExtensions", configRootsByExtensions) .data("searchable", true) .data("summaryTableId", "all-config") + .data("additionalAnchorPrefix", "") .data("includeDurationNote", true) .data("includeMemorySizeNote", true) .render(); @@ -223,150 +216,6 @@ private static void initTargetDirectory(Path resolvedTargetDirectory) throws Moj } } - private static JavadocRepository findJavadocElements(List targetDirectories) throws MojoExecutionException { - Map javadocElementsMap = new HashMap<>(); - - for (Path targetDirectory : targetDirectories) { - Path javadocPath = targetDirectory.resolve(Outputs.QUARKUS_CONFIG_DOC_JAVADOC); - if (!Files.isReadable(javadocPath)) { - continue; - } - - try { - JavadocElements javadocElements = YAML_OBJECT_MAPPER.readValue(javadocPath.toFile(), JavadocElements.class); - - if (javadocElements.elements() == null || javadocElements.elements().isEmpty()) { - continue; - } - - javadocElementsMap.putAll(javadocElements.elements()); - } catch (IOException e) { - throw new MojoExecutionException("Unable to parse: " + javadocPath, e); - } - } - - return new JavadocRepository(javadocElementsMap); - } - - private static MergedModel mergeModel(List targetDirectories) throws MojoExecutionException { - // keyed on extension and then top level prefix - Map> configRoots = new HashMap<>(); - // keyed on file name - Map configRootsInSpecificFile = new TreeMap<>(); - // keyed on extension - Map> generatedConfigSections = new TreeMap<>(); - - for (Path targetDirectory : targetDirectories) { - Path javadocPath = targetDirectory.resolve(Outputs.QUARKUS_CONFIG_DOC_MODEL); - if (!Files.isReadable(javadocPath)) { - continue; - } - - try { - ResolvedModel resolvedModel = YAML_OBJECT_MAPPER.readValue(javadocPath.toFile(), ResolvedModel.class); - - if (resolvedModel.getConfigRoots() == null || resolvedModel.getConfigRoots().isEmpty()) { - continue; - } - - for (ConfigRoot configRoot : resolvedModel.getConfigRoots()) { - if (configRoot.getOverriddenDocFileName() != null) { - ConfigRoot existingConfigRootInSpecificFile = configRootsInSpecificFile - .get(configRoot.getOverriddenDocFileName()); - - if (existingConfigRootInSpecificFile == null) { - configRootsInSpecificFile.put(configRoot.getOverriddenDocFileName(), configRoot); - } else { - if (!existingConfigRootInSpecificFile.getExtension().equals(configRoot.getExtension()) - || !existingConfigRootInSpecificFile.getPrefix().equals(configRoot.getPrefix())) { - throw new MojoExecutionException( - "Two config roots with different extensions or prefixes cannot be merged in the same specific config file: " - + configRoot.getOverriddenDocFileName()); - } - - existingConfigRootInSpecificFile.merge(configRoot); - } - - continue; - } - - String topLevelPrefix = getTopLevelPrefix(configRoot.getPrefix()); - - Map extensionConfigRoots = configRoots.computeIfAbsent(configRoot.getExtension(), - e -> new HashMap<>()); - - ConfigRoot existingConfigRoot = extensionConfigRoots.get(topLevelPrefix); - - if (existingConfigRoot == null) { - extensionConfigRoots.put(topLevelPrefix, configRoot); - } else { - existingConfigRoot.merge(configRoot); - } - } - } catch (IOException e) { - throw new MojoExecutionException("Unable to parse: " + javadocPath, e); - } - } - - configRoots = retainBestExtensionKey(configRoots); - - for (Entry> extensionConfigRootsEntry : configRoots.entrySet()) { - List extensionGeneratedConfigSections = generatedConfigSections - .computeIfAbsent(extensionConfigRootsEntry.getKey(), e -> new ArrayList<>()); - - for (ConfigRoot configRoot : extensionConfigRootsEntry.getValue().values()) { - collectGeneratedConfigSections(extensionGeneratedConfigSections, configRoot); - } - } - - return new MergedModel(configRoots, configRootsInSpecificFile, generatedConfigSections); - } - - private static Map> retainBestExtensionKey( - Map> configRoots) { - return configRoots.entrySet().stream().collect(Collectors.toMap(e -> { - Extension extension = e.getKey(); - - for (ConfigRoot configRoot : e.getValue().values()) { - if (configRoot.getExtension().nameSource().isBetterThan(extension.nameSource())) { - extension = configRoot.getExtension(); - } - if (NameSource.EXTENSION_METADATA.equals(extension.nameSource())) { - // we won't find any better - break; - } - } - - return extension; - }, e -> e.getValue(), (k1, k2) -> k1, TreeMap::new)); - } - - private static void collectGeneratedConfigSections(List extensionGeneratedConfigSections, - ConfigItemCollection configItemCollection) { - for (AbstractConfigItem configItem : configItemCollection.getItems()) { - if (!configItem.isSection()) { - continue; - } - - ConfigSection configSection = (ConfigSection) configItem; - if (configSection.isGenerated()) { - extensionGeneratedConfigSections.add(configSection); - } - - collectGeneratedConfigSections(extensionGeneratedConfigSections, configSection); - } - } - - private static String getTopLevelPrefix(String prefix) { - String[] prefixSegments = prefix.split(Pattern.quote(Markers.DOT)); - - if (prefixSegments.length == 1) { - return prefixSegments[0]; - } - - return prefixSegments[0] + Markers.DOT + prefixSegments[1]; - } - private static List findTargetDirectories(Path scanDirectory) throws MojoExecutionException { try { List targets = new ArrayList<>(); @@ -420,40 +269,57 @@ private static Engine initializeQuteEngine(AsciidocFormatter asciidocFormatter) .addValueResolver(ValueResolver.builder() .applyToBaseClass(ConfigProperty.class) .applyToName("toAnchor") - .applyToParameters(1) - .resolveAsync(ctx -> ctx.evaluate(ctx.getParams().get(0)) - .thenApply(o -> asciidocFormatter.toAnchor( - ((Extension) o).artifactId() + "_" + ((ConfigProperty) ctx.getBase()).getPath()))) + .applyToParameters(2) + .resolveSync(ctx -> asciidocFormatter + .toAnchor(((Extension) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join()) + .artifactId() + + // the additional suffix + ctx.evaluate(ctx.getParams().get(1)).toCompletableFuture().join() + + "_" + ((ConfigProperty) ctx.getBase()).getPath())) .build()) // we need a different anchor for sections as otherwise we can have a conflict // (typically when you have an `enabled` property with parent name just under the section level) .addValueResolver(ValueResolver.builder() .applyToBaseClass(ConfigSection.class) .applyToName("toAnchor") - .applyToParameters(1) - .resolveAsync(ctx -> ctx.evaluate(ctx.getParams().get(0)) - .thenApply(o -> asciidocFormatter.toAnchor( - ((Extension) o).artifactId() + "_section_" - + ((ConfigSection) ctx.getBase()).getPath()))) + .applyToParameters(2) + .resolveSync(ctx -> asciidocFormatter + .toAnchor(((Extension) ctx.evaluate(ctx.getParams().get(0)).toCompletableFuture().join()) + .artifactId() + + // the additional suffix + ctx.evaluate(ctx.getParams().get(1)).toCompletableFuture().join() + + "_section_" + ((ConfigSection) ctx.getBase()).getPath())) .build()) .addValueResolver(ValueResolver.builder() .applyToBaseClass(ConfigProperty.class) - .applyToName("typeDescription") + .applyToName("formatTypeDescription") .applyToNoParameters() .resolveSync(ctx -> asciidocFormatter.formatTypeDescription((ConfigProperty) ctx.getBase())) .build()) .addValueResolver(ValueResolver.builder() .applyToBaseClass(ConfigProperty.class) - .applyToName("description") + .applyToName("formatDescription") .applyToNoParameters() .resolveSync(ctx -> asciidocFormatter.formatDescription((ConfigProperty) ctx.getBase())) .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(ConfigProperty.class) + .applyToName("formatDefaultValue") + .applyToNoParameters() + .resolveSync(ctx -> asciidocFormatter.formatDefaultValue((ConfigProperty) ctx.getBase())) + .build()) .addValueResolver(ValueResolver.builder() .applyToBaseClass(ConfigSection.class) - .applyToName("title") + .applyToName("formatTitle") .applyToNoParameters() .resolveSync(ctx -> asciidocFormatter.formatSectionTitle((ConfigSection) ctx.getBase())) .build()) + .addValueResolver(ValueResolver.builder() + .applyToBaseClass(Extension.class) + .applyToName("formatName") + .applyToNoParameters() + .resolveSync(ctx -> asciidocFormatter.formatName((Extension) ctx.getBase())) + .build()) .build(); engine.putTemplate("configReference.qute.adoc", diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/allConfig.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/allConfig.qute.adoc index c4004ad655d26..1f8b67336d8f5 100644 --- a/devtools/config-doc-maven-plugin/src/main/resources/templates/allConfig.qute.adoc +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/allConfig.qute.adoc @@ -5,7 +5,7 @@ icon:lock[title=Fixed at build time] Configuration property fixed at build time |=== {#for extensionConfigRootsEntry in configRootsByExtensions} -h|{extensionConfigRootsEntry.key.name.or(extensionConfigRootsEntry.key.artifactId)} +h|[.extension-name]##{extensionConfigRootsEntry.key.formatName.escapeCellContent}## h|Type h|Default @@ -13,10 +13,10 @@ h|Default {#for item in configRoot.items} {#if !item.deprecated} {#if item.isSection} -{#configSection configSection=item extension=extensionConfigRootsEntry.key /} +{#configSection configSection=item extension=extensionConfigRootsEntry.key additionalAnchorPrefix=additionalAnchorPrefix /} {#else} -{#configProperty configProperty=item extension=extensionConfigRootsEntry.key /} +{#configProperty configProperty=item extension=extensionConfigRootsEntry.key additionalAnchorPrefix=additionalAnchorPrefix /} {/if} {/if} diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/configReference.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/configReference.qute.adoc index 3fe7092a6be20..d27a9195b4c00 100644 --- a/devtools/config-doc-maven-plugin/src/main/resources/templates/configReference.qute.adoc +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/configReference.qute.adoc @@ -4,17 +4,17 @@ icon:lock[title=Fixed at build time] Configuration property fixed at build time [.configuration-reference{#if searchable}.searchable{/if}, cols="80,.^10,.^10"] |=== -h|Configuration property +h|[.header-title]##Configuration property## h|Type h|Default {#for item in configItemCollection.items} {#if !item.deprecated} {#if item.isSection} -{#configSection configSection=item extension=extension /} +{#configSection configSection=item extension=extension additionalAnchorPrefix=additionalAnchorPrefix /} {#else} -{#configProperty configProperty=item extension=extension /} +{#configProperty configProperty=item extension=extension additionalAnchorPrefix=additionalAnchorPrefix /} {/if} {/if} diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configProperty.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configProperty.qute.adoc index 33d7ca0c5430d..dd3a23f0d0abc 100644 --- a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configProperty.qute.adoc +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configProperty.qute.adoc @@ -1,4 +1,4 @@ -a|{#if configProperty.phase.fixedAtBuildTime}icon:lock[title=Fixed at build time]{/if} [[{configProperty.toAnchor(extension)}]] `{configProperty.path}` +a|{#if configProperty.phase.fixedAtBuildTime}icon:lock[title=Fixed at build time]{/if} [[{configProperty.toAnchor(extension, additionalAnchorPrefix)}]] [.property-path]##`{configProperty.path}`## {#for additionalPath in configProperty.additionalPaths} `{additionalPath}` @@ -6,8 +6,8 @@ a|{#if configProperty.phase.fixedAtBuildTime}icon:lock[title=Fixed at build time [.description] -- -{configProperty.description.escapeCellContent.or("")} +{configProperty.formatDescription.escapeCellContent.or("")} {#envVar configProperty /} -- -{#if configProperty.enum}a{/if}|{configProperty.typeDescription} -|{#if configProperty.defaultValue}`{configProperty.defaultValue.escapeCellContent}`{#else if !configProperty.optional}required icon:exclamation-circle[title=Configuration property is required]{/if} +{#if configProperty.enum}a{/if}|{configProperty.formatTypeDescription.escapeCellContent.or("")} +|{#if configProperty.defaultValue}{configProperty.formatDefaultValue.escapeCellContent}{#else if !configProperty.optional}required icon:exclamation-circle[title=Configuration property is required]{/if} diff --git a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configSection.qute.adoc b/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configSection.qute.adoc index c3100613be598..d8673758bdd46 100644 --- a/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configSection.qute.adoc +++ b/devtools/config-doc-maven-plugin/src/main/resources/templates/tags/configSection.qute.adoc @@ -1,14 +1,14 @@ -h|[[{configSection.toAnchor(extension)}]] {configSection.title.escapeCellContent} +h|[[{configSection.toAnchor(extension, additionalAnchorPrefix)}]] [.section-name.section-level{configSection.level}]##{configSection.formatTitle.escapeCellContent}## h|Type h|Default {#for item in configSection.items} {#if !item.deprecated} {#if item.isSection} -{#configSection configSection=item extension=extension /} +{#configSection configSection=item extension=extension additionalAnchorPrefix=additionalAnchorPrefix /} {#else} -{#configProperty configProperty=item extension=extension /} +{#configProperty configProperty=item extension=extension additionalAnchorPrefix=additionalAnchorPrefix /} {/if} {/if} diff --git a/docs/pom.xml b/docs/pom.xml index da9bc9f05edeb..5201246f04796 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -3189,6 +3189,7 @@ ${skipDocs} true + true diff --git a/docs/src/main/asciidoc/config-reference.adoc b/docs/src/main/asciidoc/config-reference.adoc index 5ca6fb93d3191..d11d95c38d2f4 100644 --- a/docs/src/main/asciidoc/config-reference.adoc +++ b/docs/src/main/asciidoc/config-reference.adoc @@ -847,4 +847,4 @@ link:https://smallrye.io/smallrye-config/Main[SmallRye Config documentation]. == Configuration Reference -include::{generated-dir}/config/quarkus-core_quarkus-config.adoc[opts=optional, leveloffset=+1] +include::{generated-dir}/config/quarkus-core_quarkus.config.adoc[opts=optional, leveloffset=+1] diff --git a/docs/src/main/asciidoc/databases-dev-services.adoc b/docs/src/main/asciidoc/databases-dev-services.adoc index ae563a67dc2f2..8d640c4d15e57 100644 --- a/docs/src/main/asciidoc/databases-dev-services.adoc +++ b/docs/src/main/asciidoc/databases-dev-services.adoc @@ -121,7 +121,7 @@ In that case, you will need to stop and remove these containers manually. If you want to reuse containers for some Quarkus applications but not all of them, or some Dev Services but not all of them, you can disable this feature for a specific Dev Service by setting the configuration property -xref:quarkus-datasource_quarkus-datasource-devservices-reuse[`quarkus.datasource.devservices.reuse`/`quarkus.datasource."datasource-name".devservices.reuse`] +xref:quarkus-datasource_quarkus-datasource-devservices_quarkus-datasource-devservices-reuse[`quarkus.datasource.devservices.reuse`/`quarkus.datasource."datasource-name".devservices.reuse`] to `false`. == Mapping volumes into Dev Services for Database diff --git a/docs/src/main/asciidoc/elasticsearch-dev-services.adoc b/docs/src/main/asciidoc/elasticsearch-dev-services.adoc index 1f92726c7f973..48d4ec22fb249 100644 --- a/docs/src/main/asciidoc/elasticsearch-dev-services.adoc +++ b/docs/src/main/asciidoc/elasticsearch-dev-services.adoc @@ -138,7 +138,7 @@ In that case, you will need to stop and remove these containers manually. If you want to reuse containers for some Quarkus applications but not all of them, or some Dev Services but not all of them, you can disable this feature for a specific Dev Service by setting the configuration property -xref:elasticsearch-dev-services.adoc#quarkus-elasticsearch-rest-client_quarkus-elasticsearch-devservices-reuse[`quarkus.elasticsearch.devservices.reuse`] +xref:elasticsearch-dev-services.adoc#quarkus-elasticsearch-rest-client_quarkus-elasticsearch-devservices_quarkus-elasticsearch-devservices-reuse[`quarkus.elasticsearch.devservices.reuse`] to `false`. == Current limitations