From e4d6da59027b8e78c8c9b209991fc4af951e2d94 Mon Sep 17 00:00:00 2001 From: gnodet Date: Wed, 14 Oct 2026 15:40:47 +0000 Subject: [PATCH 01/16] CAMEL-25538: fix configuration-validate to accept all camel.* groups and report accepted=false as valid=false AbstractCamelCatalog.acceptConfigurationPropertyKey() hardcoded only ten camel.* prefixes (component/dataformat/language/main/resilience4j/ faulttolerance/threadpool/health/lra/rest), so keys under the 26 other groups defined in camel-main-configuration-metadata.json (camel.server.*, camel.management.*, camel.ssl.*, camel.debug.*, camel.trace.*, camel.vault.*, camel.cluster.*, camel.routecontroller.*, and others) were rejected with accepted=false. Because validateConfigurationProperty() returned immediately after setAccepted(false) without recording any error, isSuccess() returned true, producing the contradictory response {accepted:false, valid:true} for every unrecognised camel.* group. Fix 1 (AbstractCamelCatalog): replace the hardcoded prefix list with a dynamic lookup against mainModel().getGroups() so that any group declared in the metadata is automatically accepted without needing a code change. component/dataformat/language keep their own static prefixes because they use a different per-artifact model, not mainModel(). Fix 2 (ConfigurationValidateTools): at the MCP layer, guard valid with "accepted && result.isSuccess()" so that an unaccepted key is never reported as valid, even when the catalog returned no errors. Co-authored-by: Claude Sonnet 4.6 --- .../catalog/impl/AbstractCamelCatalog.java | 104 ++++++++++++------ .../mcp/ConfigurationValidateTools.java | 6 +- .../mcp/ConfigurationValidateToolsTest.java | 77 +++++++++++++ 3 files changed, 150 insertions(+), 37 deletions(-) diff --git a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java index 896cb50400e88..28f5488ab825f 100644 --- a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java +++ b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java @@ -1232,33 +1232,39 @@ public ConfigurationPropertiesValidationResult validateConfigurationProperty(Str int posBracket = nOption.indexOf('['); validateConfigurationProperty(posDot, posBracket, suffix, nOption, result, rows, name, value, longKey); } - } else if (key.startsWith("main.") - || key.startsWith("resilience4j.") - || key.startsWith("faulttolerance.") - || key.startsWith("threadpool.") - || key.startsWith("lra.") - || key.startsWith("health.") - || key.startsWith("rest.")) { - String name = StringHelper.before(key, "."); - if (value != null) { - MainModel model = mainModel(); - if (model == null) { - result.addIncapable("camel-main not detected on classpath"); - return result; - } - Map rows = new HashMap<>(); - model.getOptions().forEach(o -> rows.put(dashToCamelCase(o.getName()), o)); - - // lower case option and remove dash - String nOption = longKey.replace("-", "").toLowerCase(Locale.ENGLISH); - - // look for suffix or array index after 2nd dot - int secondDot = nOption.indexOf('.', nOption.indexOf('.') + 1) + 1; + } else { + // Check if the key matches a main-model group (dynamic — replaces the hardcoded list) + MainModel.MainGroupModel matchedGroup = matchMainModelGroup(key); + if (matchedGroup != null) { + String name = StringHelper.before(key, "."); + if (value != null) { + MainModel model = mainModel(); + if (model == null) { + result.addIncapable("camel-main not detected on classpath"); + return result; + } + Map rows = new HashMap<>(); + model.getOptions().forEach(o -> rows.put(dashToCamelCase(o.getName()), o)); + + // lower case option and remove dash + String nOption = longKey.replace("-", "").toLowerCase(Locale.ENGLISH); + + // look for suffix or array index after the group prefix + // group name depth tells us how many dots to skip: "camel.vault.aws" has 2 dots, + // so the option starts after the 3rd dot in the full key + int groupDots = (int) matchedGroup.getName().chars().filter(c -> c == '.').count(); + int pos = 0; + for (int d = 0; d <= groupDots; d++) { + pos = nOption.indexOf('.', pos) + 1; + } + // pos now points just after the group prefix in nOption (e.g. after "camel.server.") + int afterGroup = pos; - String suffix = null; - int posDot = nOption.indexOf('.', secondDot); - int posBracket = nOption.indexOf('[', secondDot); - validateConfigurationProperty(posDot, posBracket, suffix, nOption, result, rows, name, value, longKey); + String suffix = null; + int posDot = nOption.indexOf('.', afterGroup); + int posBracket = nOption.indexOf('[', afterGroup); + validateConfigurationProperty(posDot, posBracket, suffix, nOption, result, rows, name, value, longKey); + } } } @@ -1408,20 +1414,46 @@ private static boolean isValidSuffix(String suffix) { return suffix == null || suffix.isEmpty() || suffix.equals("."); } - private static boolean acceptConfigurationPropertyKey(String key) { + private boolean acceptConfigurationPropertyKey(String key) { if (key == null) { return false; } - return key.startsWith("camel.component.") + // Always-accepted static prefixes (component/dataformat/language have their own models) + if (key.startsWith("camel.component.") || key.startsWith("camel.dataformat.") - || key.startsWith("camel.language.") - || key.startsWith("camel.main.") - || key.startsWith("camel.resilience4j.") - || key.startsWith("camel.faulttolerance.") - || key.startsWith("camel.threadpool.") - || key.startsWith("camel.health.") - || key.startsWith("camel.lra.") - || key.startsWith("camel.rest."); + || key.startsWith("camel.language.")) { + return true; + } + // Dynamic: accept any prefix that matches a group in the main-model metadata + MainModel model = mainModel(); + if (model != null) { + for (MainModel.MainGroupModel group : model.getGroups()) { + String groupPrefix = group.getName() + "."; + if (key.startsWith(groupPrefix)) { + return true; + } + } + } + return false; + } + + /** + * Returns the matched group from the main-model metadata when the key (with the leading {@code camel.} stripped) + * corresponds to one of the registered groups, or {@code null} if no group matches. + */ + private MainModel.MainGroupModel matchMainModelGroup(String key) { + MainModel model = mainModel(); + if (model == null) { + return null; + } + for (MainModel.MainGroupModel group : model.getGroups()) { + // group name is e.g. "camel.server" — strip the leading "camel." to get "server" + String groupName = group.getName().substring("camel.".length()); + if (key.startsWith(groupName + ".")) { + return group; + } + } + return null; } /** diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java index de13bfd8647b6..66a9452c90ecb 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java @@ -106,8 +106,12 @@ public ConfigurationValidateResult camel_configuration_validate( private PropertyValidationInfo toPropertyValidationInfo( int lineNumber, String text, ConfigurationPropertiesValidationResult result) { - boolean valid = result.isSuccess(); boolean accepted = result.isAccepted(); + // A key that is not accepted by the catalog is by definition not valid: the catalog + // returned no errors (because it returned early without checking anything), but the + // property is outside the set of known camel.* groups, so reporting valid=true would + // be misleading to the caller. + boolean valid = accepted && result.isSuccess(); int errors = result.getNumberOfErrors(); int warnings = result.getNumberOfWarnings(); diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java index bf1a6159faae0..aebe74326a3eb 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java @@ -180,6 +180,83 @@ void nonCamelKeyMarkedNotAccepted() { ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); assertThat(info.accepted()).isFalse(); + // accepted=false must imply valid=false (regression guard for CAMEL-25538 bug #2) + assertThat(info.valid()).isFalse(); + } + + // --- CAMEL-25538 regression tests --- + + @Test + void camelServerPortIsAcceptedAndValid() { + // camel.server.* was previously not in the hardcoded prefix list — it must now be + // accepted and validated against the main-model metadata. + ConfigurationValidateTools tools = createTools(); + + ConfigurationValidateTools.ConfigurationValidateResult result + = tools.camel_configuration_validate("camel.server.port=9999", null, null, null); + + assertThat(result.summary().total()).isEqualTo(1); + ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); + assertThat(info.accepted()).isTrue(); + assertThat(info.valid()).isTrue(); + assertThat(info.errors()).isZero(); + assertThat(info.issues()).isEmpty(); + } + + @Test + void camelServerInventedKeyIsAcceptedButNotValid() { + // An invented key under an accepted camel.server.* group must be reported as unknown, + // not silently passed as valid. + ConfigurationValidateTools tools = createTools(); + + ConfigurationValidateTools.ConfigurationValidateResult result + = tools.camel_configuration_validate("camel.server.bindPort=9999", null, null, null); + + ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); + assertThat(info.accepted()).isTrue(); + assertThat(info.valid()).isFalse(); + assertThat(info.issues()).anyMatch(i -> "unknown".equals(i.kind())); + } + + @Test + void acceptedFalseImpliesValidFalseForNonCamelKey() { + // Invented key outside any known camel.* group: accepted=false AND valid=false. + // Before the fix, valid was true despite accepted=false (CAMEL-25538 bug #2). + ConfigurationValidateTools tools = createTools(); + + ConfigurationValidateTools.ConfigurationValidateResult result + = tools.camel_configuration_validate("camel.invented.completely.unknown=foo", null, null, null); + + ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); + assertThat(info.accepted()).isFalse(); + assertThat(info.valid()).isFalse(); + // The summary must not count unaccepted keys as valid + assertThat(result.summary().valid()).isZero(); + } + + @Test + void camelManagementKeyIsAccepted() { + // camel.management.* was also missing from the old hardcoded list. + ConfigurationValidateTools tools = createTools(); + + ConfigurationValidateTools.ConfigurationValidateResult result + = tools.camel_configuration_validate("camel.management.enabled=true", null, null, null); + + ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); + assertThat(info.accepted()).isTrue(); + assertThat(info.valid()).isTrue(); + } + + @Test + void camelVaultAwsKeyIsAccepted() { + // camel.vault.aws.* is a nested group (3-segment name) — must also be accepted. + ConfigurationValidateTools tools = createTools(); + + ConfigurationValidateTools.ConfigurationValidateResult result + = tools.camel_configuration_validate("camel.vault.aws.accessKey=AKIA1234", null, null, null); + + ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); + assertThat(info.accepted()).isTrue(); } @Test From 8966e92b7d25f5af731e38b02456d3c6250916b9 Mon Sep 17 00:00:00 2001 From: Guillaume Nodet Date: Fri, 16 Oct 2026 06:30:26 +0000 Subject: [PATCH 02/16] =?UTF-8?q?CAMEL-25538:=20address=20review=20?= =?UTF-8?q?=E2=80=94=20checked/unchecked=20semantics,=20dash-insensitive?= =?UTF-8?q?=20group=20matching,=20single=20model=20load?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../catalog/impl/AbstractCamelCatalog.java | 42 ++++++++++---- .../mcp/ConfigurationValidateTools.java | 53 ++++++++++------- .../mcp/ConfigurationValidateToolsTest.java | 57 +++++++++++++------ 3 files changed, 103 insertions(+), 49 deletions(-) diff --git a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java index 28f5488ab825f..5547d35dbf39b 100644 --- a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java +++ b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java @@ -1185,7 +1185,10 @@ public ConfigurationPropertiesValidationResult validateConfigurationProperty(Str } ConfigurationPropertiesValidationResult result = new ConfigurationPropertiesValidationResult(); - boolean accept = acceptConfigurationPropertyKey(key); + // Load the main model once here to avoid repeated JSON parsing in acceptConfigurationPropertyKey + // and matchMainModelGroup (both called for camel.* keys that are not component/dataformat/language). + MainModel cachedMainModel = key != null && key.startsWith("camel.") ? mainModel() : null; + boolean accept = acceptConfigurationPropertyKey(key, cachedMainModel); if (!accept) { result.setAccepted(false); return result; @@ -1234,11 +1237,14 @@ public ConfigurationPropertiesValidationResult validateConfigurationProperty(Str } } else { // Check if the key matches a main-model group (dynamic — replaces the hardcoded list) - MainModel.MainGroupModel matchedGroup = matchMainModelGroup(key); + MainModel.MainGroupModel matchedGroup = matchMainModelGroup(key, cachedMainModel); if (matchedGroup != null) { String name = StringHelper.before(key, "."); if (value != null) { - MainModel model = mainModel(); + // cachedMainModel is guaranteed non-null here: acceptConfigurationPropertyKey returned true + // for a camel.* key that is not component/dataformat/language, which means matchMainModelGroup + // already found a match — so the model must have been available. + MainModel model = cachedMainModel; if (model == null) { result.addIncapable("camel-main not detected on classpath"); return result; @@ -1414,22 +1420,30 @@ private static boolean isValidSuffix(String suffix) { return suffix == null || suffix.isEmpty() || suffix.equals("."); } - private boolean acceptConfigurationPropertyKey(String key) { + private boolean acceptConfigurationPropertyKey(String key, MainModel cachedModel) { if (key == null) { return false; } + // Keys outside the camel.* namespace are never checked by this catalog + if (!key.startsWith("camel.")) { + return false; + } // Always-accepted static prefixes (component/dataformat/language have their own models) if (key.startsWith("camel.component.") || key.startsWith("camel.dataformat.") || key.startsWith("camel.language.")) { return true; } - // Dynamic: accept any prefix that matches a group in the main-model metadata - MainModel model = mainModel(); + // Dynamic: accept any prefix that matches a group in the main-model metadata. + // camel-main binds keys after normalising dashes and case (MainHelper.optionKey / + // startsWithIgnoreCase), so we compare normalised forms to match e.g. + // camel.error-registry.* against the group name "camel.errorRegistry". + String normKey = key.replace("-", "").toLowerCase(Locale.ENGLISH); + MainModel model = cachedModel != null ? cachedModel : mainModel(); if (model != null) { for (MainModel.MainGroupModel group : model.getGroups()) { - String groupPrefix = group.getName() + "."; - if (key.startsWith(groupPrefix)) { + String normGroup = group.getName().replace("-", "").toLowerCase(Locale.ENGLISH) + "."; + if (normKey.startsWith(normGroup)) { return true; } } @@ -1440,16 +1454,22 @@ private boolean acceptConfigurationPropertyKey(String key) { /** * Returns the matched group from the main-model metadata when the key (with the leading {@code camel.} stripped) * corresponds to one of the registered groups, or {@code null} if no group matches. + *

+ * Comparison is normalised (dashes removed, lower-cased) to mirror how camel-main binds keys at runtime via + * {@code MainHelper.optionKey} and {@code startsWithIgnoreCase}. */ - private MainModel.MainGroupModel matchMainModelGroup(String key) { - MainModel model = mainModel(); + private MainModel.MainGroupModel matchMainModelGroup(String key, MainModel cachedModel) { + MainModel model = cachedModel != null ? cachedModel : mainModel(); if (model == null) { return null; } + // Normalise the incoming key once + String normKey = key.replace("-", "").toLowerCase(Locale.ENGLISH); for (MainModel.MainGroupModel group : model.getGroups()) { // group name is e.g. "camel.server" — strip the leading "camel." to get "server" String groupName = group.getName().substring("camel.".length()); - if (key.startsWith(groupName + ".")) { + String normGroupName = groupName.replace("-", "").toLowerCase(Locale.ENGLISH); + if (normKey.startsWith(normGroupName + ".")) { return group; } } diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java index 66a9452c90ecb..e04bc5df10815 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java @@ -53,7 +53,13 @@ public class ConfigurationValidateTools { + "returns suggestions when available. The input can be a single line or a multi-line string with " + - "one property per line; blank lines and comments (starting with # or !) are skipped.") + "one property per line; blank lines and comments (starting with # or !) are skipped. " + + + "Lines that are not recognised as Camel properties (e.g. server.port, quarkus.* or camel.beans.*) " + + + "are reported with checked=false and are excluded from summary.valid and summary.errors; " + + + "they are counted in summary.unchecked instead.") public ConfigurationValidateResult camel_configuration_validate( @ToolArg(description = "Configuration property lines to validate. Can be a single line " + "(e.g. \"camel.main.streamCaching=true\") or multiple lines separated by newlines.") String properties, @@ -72,6 +78,7 @@ public ConfigurationValidateResult camel_configuration_validate( int totalErrors = 0; int totalWarnings = 0; int validCount = 0; + int uncheckedCount = 0; String[] lines = properties.split("\\r?\\n"); for (int i = 0; i < lines.length; i++) { @@ -84,15 +91,20 @@ public ConfigurationValidateResult camel_configuration_validate( ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(line); PropertyValidationInfo info = toPropertyValidationInfo(i + 1, line, result); results.add(info); - totalErrors += info.errors(); - totalWarnings += info.warnings(); - if (info.valid()) { - validCount++; + if (!info.checked()) { + uncheckedCount++; + } else { + totalErrors += info.errors(); + totalWarnings += info.warnings(); + if (info.valid()) { + validCount++; + } } } ConfigurationValidateSummary summary - = new ConfigurationValidateSummary(results.size(), validCount, totalErrors, totalWarnings); + = new ConfigurationValidateSummary(results.size(), validCount, totalErrors, totalWarnings, + uncheckedCount); return new ConfigurationValidateResult(catalog.getCatalogVersion(), summary, results); } catch (ToolCallException e) { @@ -106,19 +118,18 @@ public ConfigurationValidateResult camel_configuration_validate( private PropertyValidationInfo toPropertyValidationInfo( int lineNumber, String text, ConfigurationPropertiesValidationResult result) { - boolean accepted = result.isAccepted(); - // A key that is not accepted by the catalog is by definition not valid: the catalog - // returned no errors (because it returned early without checking anything), but the - // property is outside the set of known camel.* groups, so reporting valid=true would - // be misleading to the caller. - boolean valid = accepted && result.isSuccess(); - int errors = result.getNumberOfErrors(); - int warnings = result.getNumberOfWarnings(); - - List issues = collectIssues(result); - String summary = result.summaryErrorMessage(false, true, true); - - return new PropertyValidationInfo(lineNumber, text, accepted, valid, errors, warnings, summary, issues); + boolean checked = result.isAccepted(); + // A checked line is valid when the catalog found no errors; an unchecked line + // (not recognised as a Camel property) is neither valid nor invalid — it is simply + // not inspected by this catalog. + boolean valid = checked && result.isSuccess(); + int errors = checked ? result.getNumberOfErrors() : 0; + int warnings = checked ? result.getNumberOfWarnings() : 0; + + List issues = checked ? collectIssues(result) : Collections.emptyList(); + String summary = checked ? result.summaryErrorMessage(false, true, true) : null; + + return new PropertyValidationInfo(lineNumber, text, checked, valid, errors, warnings, summary, issues); } private List collectIssues(ConfigurationPropertiesValidationResult result) { @@ -207,10 +218,10 @@ public record ConfigurationValidateResult(String camelVersion, ConfigurationVali List properties) { } - public record ConfigurationValidateSummary(int total, int valid, int errors, int warnings) { + public record ConfigurationValidateSummary(int total, int valid, int errors, int warnings, int unchecked) { } - public record PropertyValidationInfo(int lineNumber, String text, boolean accepted, boolean valid, int errors, + public record PropertyValidationInfo(int lineNumber, String text, boolean checked, boolean valid, int errors, int warnings, String summary, List issues) { } diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java index aebe74326a3eb..e9a3504889b1e 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java @@ -51,7 +51,7 @@ void validatesValidMainProperty() { ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); assertThat(info.lineNumber()).isEqualTo(1); - assertThat(info.accepted()).isTrue(); + assertThat(info.checked()).isTrue(); assertThat(info.valid()).isTrue(); assertThat(info.errors()).isZero(); assertThat(info.issues()).isEmpty(); @@ -113,7 +113,7 @@ void acceptsPlaceholderValuesAcrossTypes() { assertThat(result.summary().errors()).isZero(); assertThat(result.properties()) .allSatisfy(info -> { - assertThat(info.accepted()).isTrue(); + assertThat(info.checked()).isTrue(); assertThat(info.valid()).isTrue(); assertThat(info.errors()).isZero(); assertThat(info.issues()).isEmpty(); @@ -130,7 +130,7 @@ void acceptsSpringStylePlaceholderValue() { null); ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); - assertThat(info.accepted()).isTrue(); + assertThat(info.checked()).isTrue(); assertThat(info.valid()).isTrue(); assertThat(info.errors()).isZero(); assertThat(info.issues()).isEmpty(); @@ -172,16 +172,20 @@ void multipleLinesAggregated() { } @Test - void nonCamelKeyMarkedNotAccepted() { + void nonCamelKeyMarkedNotChecked() { + // Non-Camel keys (e.g. server.port) are not checked by this catalog: checked=false, + // valid=false, and counted in summary.unchecked (not in summary.errors or summary.valid). ConfigurationValidateTools tools = createTools(); ConfigurationValidateTools.ConfigurationValidateResult result = tools.camel_configuration_validate("server.port=8080", null, null, null); ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); - assertThat(info.accepted()).isFalse(); - // accepted=false must imply valid=false (regression guard for CAMEL-25538 bug #2) + assertThat(info.checked()).isFalse(); assertThat(info.valid()).isFalse(); + assertThat(result.summary().unchecked()).isEqualTo(1); + assertThat(result.summary().errors()).isZero(); + assertThat(result.summary().valid()).isZero(); } // --- CAMEL-25538 regression tests --- @@ -197,14 +201,14 @@ void camelServerPortIsAcceptedAndValid() { assertThat(result.summary().total()).isEqualTo(1); ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); - assertThat(info.accepted()).isTrue(); + assertThat(info.checked()).isTrue(); assertThat(info.valid()).isTrue(); assertThat(info.errors()).isZero(); assertThat(info.issues()).isEmpty(); } @Test - void camelServerInventedKeyIsAcceptedButNotValid() { + void camelServerInventedKeyIsCheckedButNotValid() { // An invented key under an accepted camel.server.* group must be reported as unknown, // not silently passed as valid. ConfigurationValidateTools tools = createTools(); @@ -213,29 +217,32 @@ void camelServerInventedKeyIsAcceptedButNotValid() { = tools.camel_configuration_validate("camel.server.bindPort=9999", null, null, null); ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); - assertThat(info.accepted()).isTrue(); + assertThat(info.checked()).isTrue(); assertThat(info.valid()).isFalse(); assertThat(info.issues()).anyMatch(i -> "unknown".equals(i.kind())); } @Test - void acceptedFalseImpliesValidFalseForNonCamelKey() { - // Invented key outside any known camel.* group: accepted=false AND valid=false. + void uncheckedKeyImpliesValidFalseAndNotCountedInErrors() { + // Invented key outside any known camel.* group: checked=false AND valid=false. // Before the fix, valid was true despite accepted=false (CAMEL-25538 bug #2). + // The key must not appear in summary.errors — it is unchecked, not rejected. ConfigurationValidateTools tools = createTools(); ConfigurationValidateTools.ConfigurationValidateResult result = tools.camel_configuration_validate("camel.invented.completely.unknown=foo", null, null, null); ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); - assertThat(info.accepted()).isFalse(); + assertThat(info.checked()).isFalse(); assertThat(info.valid()).isFalse(); - // The summary must not count unaccepted keys as valid + // The summary must not count unchecked keys as valid or as errors assertThat(result.summary().valid()).isZero(); + assertThat(result.summary().errors()).isZero(); + assertThat(result.summary().unchecked()).isEqualTo(1); } @Test - void camelManagementKeyIsAccepted() { + void camelManagementKeyIsCheckedAndValid() { // camel.management.* was also missing from the old hardcoded list. ConfigurationValidateTools tools = createTools(); @@ -243,20 +250,36 @@ void camelManagementKeyIsAccepted() { = tools.camel_configuration_validate("camel.management.enabled=true", null, null, null); ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); - assertThat(info.accepted()).isTrue(); + assertThat(info.checked()).isTrue(); assertThat(info.valid()).isTrue(); } @Test - void camelVaultAwsKeyIsAccepted() { + void camelVaultAwsKeyIsCheckedAndValid() { // camel.vault.aws.* is a nested group (3-segment name) — must also be accepted. + // The dot-offset logic that skips the group prefix is exercised here. ConfigurationValidateTools tools = createTools(); ConfigurationValidateTools.ConfigurationValidateResult result = tools.camel_configuration_validate("camel.vault.aws.accessKey=AKIA1234", null, null, null); ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); - assertThat(info.accepted()).isTrue(); + assertThat(info.checked()).isTrue(); + assertThat(info.valid()).isTrue(); + } + + @Test + void camelVaultAwsInventedKeyIsCheckedButNotValid() { + // An invented option under a nested group (camel.vault.aws.*) exercises the dot-offset + // logic: the option name starts after the 3rd dot and must be checked against the model. + ConfigurationValidateTools tools = createTools(); + + ConfigurationValidateTools.ConfigurationValidateResult result + = tools.camel_configuration_validate("camel.vault.aws.inventedOption=foo", null, null, null); + + ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); + assertThat(info.checked()).isTrue(); + assertThat(info.valid()).isFalse(); } @Test From 83e099351c74b630ea5615d7382173f86de107cb Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 06:48:55 +0000 Subject: [PATCH 03/16] style: apply spotless formatting --- .../jbang/core/commands/mcp/ConfigurationValidateTools.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java index e04bc5df10815..5c9c5b113bb49 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java @@ -103,7 +103,8 @@ public ConfigurationValidateResult camel_configuration_validate( } ConfigurationValidateSummary summary - = new ConfigurationValidateSummary(results.size(), validCount, totalErrors, totalWarnings, + = new ConfigurationValidateSummary( + results.size(), validCount, totalErrors, totalWarnings, uncheckedCount); return new ConfigurationValidateResult(catalog.getCatalogVersion(), summary, results); From 319beb340f5c29d2abd72f783c8142c1f2e1f584 Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 07:09:51 +0000 Subject: [PATCH 04/16] =?UTF-8?q?CAMEL-25538:=20address=20review=20?= =?UTF-8?q?=E2=80=94=20skip=20main-model=20load=20for=20component/dataform?= =?UTF-8?q?at/language=20keys,=20add=20dashed-group=20MCP=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../catalog/impl/AbstractCamelCatalog.java | 21 +++++++++---------- .../mcp/ConfigurationValidateToolsTest.java | 13 ++++++++++++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java index 5547d35dbf39b..46aea305c70a7 100644 --- a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java +++ b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java @@ -1186,8 +1186,13 @@ public ConfigurationPropertiesValidationResult validateConfigurationProperty(Str ConfigurationPropertiesValidationResult result = new ConfigurationPropertiesValidationResult(); // Load the main model once here to avoid repeated JSON parsing in acceptConfigurationPropertyKey - // and matchMainModelGroup (both called for camel.* keys that are not component/dataformat/language). - MainModel cachedMainModel = key != null && key.startsWith("camel.") ? mainModel() : null; + // and matchMainModelGroup. Only load it for keys that may reach the main-group branch: + // component/dataformat/language are accepted by a static prefix check and never use the model. + MainModel cachedMainModel = key != null && key.startsWith("camel.") + && !key.startsWith("camel.component.") + && !key.startsWith("camel.dataformat.") + && !key.startsWith("camel.language.") + ? mainModel() : null; boolean accept = acceptConfigurationPropertyKey(key, cachedMainModel); if (!accept) { result.setAccepted(false); @@ -1241,16 +1246,10 @@ public ConfigurationPropertiesValidationResult validateConfigurationProperty(Str if (matchedGroup != null) { String name = StringHelper.before(key, "."); if (value != null) { - // cachedMainModel is guaranteed non-null here: acceptConfigurationPropertyKey returned true - // for a camel.* key that is not component/dataformat/language, which means matchMainModelGroup - // already found a match — so the model must have been available. - MainModel model = cachedMainModel; - if (model == null) { - result.addIncapable("camel-main not detected on classpath"); - return result; - } + // cachedMainModel is guaranteed non-null here: a match in matchMainModelGroup + // implies the model was available (the groups come from the model itself). Map rows = new HashMap<>(); - model.getOptions().forEach(o -> rows.put(dashToCamelCase(o.getName()), o)); + cachedMainModel.getOptions().forEach(o -> rows.put(dashToCamelCase(o.getName()), o)); // lower case option and remove dash String nOption = longKey.replace("-", "").toLowerCase(Locale.ENGLISH); diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java index e9a3504889b1e..88c65eaff478f 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java @@ -282,6 +282,19 @@ void camelVaultAwsInventedKeyIsCheckedButNotValid() { assertThat(info.valid()).isFalse(); } + @Test + void dashedGroupNameIsCheckedAndValid() { + // camel-main binds camel.error-registry.* to the camel.errorRegistry group + ConfigurationValidateTools tools = createTools(); + + ConfigurationValidateTools.ConfigurationValidateResult result + = tools.camel_configuration_validate("camel.error-registry.enabled=true", null, null, null); + + ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); + assertThat(info.checked()).isTrue(); + assertThat(info.valid()).isTrue(); + } + @Test void blankInputThrows() { ConfigurationValidateTools tools = createTools(); From bb8df37b18e8725caf2c9e3ab8f8d1bebf87be59 Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 07:21:34 +0000 Subject: [PATCH 05/16] =?UTF-8?q?CAMEL-25538:=20address=20review=20?= =?UTF-8?q?=E2=80=94=20upgrade=20guide=20wire-format=20note,=20matchMainMo?= =?UTF-8?q?delGroup=20Javadoc,=20vault=20test=20assertion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../catalog/impl/AbstractCamelCatalog.java | 7 ++++- .../pages/camel-4x-upgrade-guide-4_24.adoc | 27 +++++++++++++++++++ .../mcp/ConfigurationValidateToolsTest.java | 1 + 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java index 46aea305c70a7..eb7879e2977c2 100644 --- a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java +++ b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java @@ -1456,9 +1456,14 @@ private boolean acceptConfigurationPropertyKey(String key, MainModel cachedModel *

* Comparison is normalised (dashes removed, lower-cased) to mirror how camel-main binds keys at runtime via * {@code MainHelper.optionKey} and {@code startsWithIgnoreCase}. + *

+ * Note: {@link #acceptConfigurationPropertyKey} also iterates the groups with the same normalisation, but it + * receives the full {@code camel.*} key (not the stripped form) and returns a boolean rather than the matched + * group. A direct delegation is awkward because the two callers need different key conventions, so both methods are + * kept separate and share the normalisation comment above. */ private MainModel.MainGroupModel matchMainModelGroup(String key, MainModel cachedModel) { - MainModel model = cachedModel != null ? cachedModel : mainModel(); + MainModel model = cachedModel; if (model == null) { return null; } diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index baddc8dc32d65..346cbe49a3c63 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -13,6 +13,33 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] page for details. == Upgrading Camel 4.23 to 4.24 +=== camel-report-maven-plugin:validate / configuration-validate + +The catalog now accepts all `camel.*` groups defined in `camel-main-configuration-metadata.json` +(previously only 10 groups out of 36 were recognized). As a result, +`camel-report-maven-plugin:validate` and the camel-jbang MCP `camel_configuration_validate` tool +now validate approximately 26 additional `camel.*` groups (including `camel.server.*`, +`camel.management.*`, `camel.vault.*`, `camel.ssl.*`, and others). + +Group names are now matched with dash- and case-insensitive normalisation, matching how +camel-main binds keys at runtime. For example, `camel.error-registry.enabled` and +`camel.ai-observability.enabled` are now accepted alongside their camel-case equivalents. + +If your project uses `camel-report-maven-plugin:validate` with `failOnError=true`, +a key that was silently ignored before (e.g. a misspelled option under one of the newly accepted +groups) may now cause the build to fail. Review the validation output and correct any misspelled keys. + +The `camel_configuration_validate` MCP tool response format has changed: + +* `PropertyValidationInfo.accepted` is renamed to `checked`. +* `ConfigurationValidateSummary` gains a new `unchecked` integer field counting lines whose + namespace is not recognized as a Camel property (e.g. `server.port=8080` or `quarkus.*` keys). + Such lines had no `accepted` flag before; they are now explicitly classified as `checked=false` + and excluded from `summary.valid` and `summary.errors`. + +If you call `camel_configuration_validate` programmatically and read the JSON response, update +field references from `accepted` to `checked` and handle the new `summary.unchecked` field. + === camel-jbang - camel validate and camel overview report the same endpoints no route consumes `camel validate source` and `camel overview` now use the same rules for a `direct:`, `seda:` or `disruptor:` endpoint diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java index 88c65eaff478f..263000f504b18 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java @@ -266,6 +266,7 @@ void camelVaultAwsKeyIsCheckedAndValid() { ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); assertThat(info.checked()).isTrue(); assertThat(info.valid()).isTrue(); + assertThat(info.issues()).isEmpty(); } @Test From ce48265b341915b44a66cc9a89c7fdd9c8a16dea Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 07:41:30 +0000 Subject: [PATCH 06/16] =?UTF-8?q?CAMEL-25538:=20address=20review=20?= =?UTF-8?q?=E2=80=94=20remove=20dead=20model=20fallback,=20fix=20Spring=20?= =?UTF-8?q?Boot=20false=20positives=20for=20starter-only=20options?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../catalog/impl/AbstractCamelCatalog.java | 2 +- .../mcp/ConfigurationValidateTools.java | 40 +++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java index eb7879e2977c2..773152107856c 100644 --- a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java +++ b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java @@ -1438,7 +1438,7 @@ private boolean acceptConfigurationPropertyKey(String key, MainModel cachedModel // startsWithIgnoreCase), so we compare normalised forms to match e.g. // camel.error-registry.* against the group name "camel.errorRegistry". String normKey = key.replace("-", "").toLowerCase(Locale.ENGLISH); - MainModel model = cachedModel != null ? cachedModel : mainModel(); + MainModel model = cachedModel; if (model != null) { for (MainModel.MainGroupModel group : model.getGroups()) { String normGroup = group.getName().replace("-", "").toLowerCase(Locale.ENGLISH) + "."; diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java index 5c9c5b113bb49..c31367c9bac87 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -59,7 +60,11 @@ public class ConfigurationValidateTools { + "are reported with checked=false and are excluded from summary.valid and summary.errors; " + - "they are counted in summary.unchecked instead.") + "they are counted in summary.unchecked instead. " + + + "For runtime=spring-boot, starter-specific options (e.g. camel.metrics.uriTagEnabled) " + + + "are not present in the camel-main metadata and are also reported as checked=false.") public ConfigurationValidateResult camel_configuration_validate( @ToolArg(description = "Configuration property lines to validate. Can be a single line " + "(e.g. \"camel.main.streamCaching=true\") or multiple lines separated by newlines.") String properties, @@ -80,6 +85,13 @@ public ConfigurationValidateResult camel_configuration_validate( int validCount = 0; int uncheckedCount = 0; + // For spring-boot, starter-specific options (e.g. camel.metrics.uriTagEnabled from + // camel-micrometer-starter) are not present in the camel-main metadata that the catalog + // uses. The catalog accepts the group (camel.metrics is a valid main-model group) but + // then reports "Unknown option" for starter-only properties. Treat such lines as + // unchecked (not inspected) rather than invalid, to avoid false positives. + boolean isSpringBoot = "spring-boot".equalsIgnoreCase(runtime); + String[] lines = properties.split("\\r?\\n"); for (int i = 0; i < lines.length; i++) { String line = lines[i]; @@ -89,7 +101,7 @@ public ConfigurationValidateResult camel_configuration_validate( } ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(line); - PropertyValidationInfo info = toPropertyValidationInfo(i + 1, line, result); + PropertyValidationInfo info = toPropertyValidationInfo(i + 1, line, result, isSpringBoot); results.add(info); if (!info.checked()) { uncheckedCount++; @@ -118,12 +130,34 @@ public ConfigurationValidateResult camel_configuration_validate( } private PropertyValidationInfo toPropertyValidationInfo( - int lineNumber, String text, ConfigurationPropertiesValidationResult result) { + int lineNumber, String text, ConfigurationPropertiesValidationResult result, boolean isSpringBoot) { boolean checked = result.isAccepted(); // A checked line is valid when the catalog found no errors; an unchecked line // (not recognised as a Camel property) is neither valid nor invalid — it is simply // not inspected by this catalog. boolean valid = checked && result.isSuccess(); + + // For Spring Boot, the catalog uses camel-main metadata which does not include + // starter-specific options (e.g. camel.metrics.uriTagEnabled from camel-micrometer-starter). + // When the only issues are "unknown option" errors, downgrade to unchecked to avoid + // false positives — the starter may legitimately bind options outside the main schema. + if (isSpringBoot && checked && !valid) { + Set unknownKeys = result.getUnknown() != null ? result.getUnknown() : Collections.emptySet(); + boolean onlyUnknownOptionErrors = !unknownKeys.isEmpty() + && result.getNumberOfErrors() == unknownKeys.size() + && result.getSyntaxError() == null + && result.getIncapable() == null + && result.getInvalidBoolean() == null + && result.getInvalidEnum() == null + && result.getInvalidInteger() == null + && result.getInvalidNumber() == null + && result.getInvalidDuration() == null; + if (onlyUnknownOptionErrors) { + checked = false; + valid = false; + } + } + int errors = checked ? result.getNumberOfErrors() : 0; int warnings = checked ? result.getNumberOfWarnings() : 0; From 6976c39f85b283719388c62d773eb42a97fe3b26 Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 08:11:37 +0000 Subject: [PATCH 07/16] CAMEL-25538: add CamelCatalogTest direct tests for dynamic group matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three direct tests to CamelCatalogTest that exercise AbstractCamelCatalog's dynamic group matching logic independently of ConfigurationValidateTools: - testValidateConfigurationPropertyNestedGroup: validates nested camel.vault.aws.* keys and confirms invented options are rejected - testValidateConfigurationPropertyDashedGroupName: validates dash-insensitive group matching (camel.error-registry.* → camel.errorRegistry group) - testValidateConfigurationPropertyServerInventedOption: validates that camel.server.* accepts known options and rejects invented ones --- .../camel/catalog/CamelCatalogTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java index e75d2a90d1fef..0930cd1553442 100644 --- a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java +++ b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java @@ -1976,6 +1976,64 @@ public void testValidateConfigurationPropertyMainMap() { assertTrue(result.isSuccess()); } + @Test + public void testValidateConfigurationPropertyNestedGroup() { + // camel.vault.aws.* is a nested group (3-segment prefix) — the catalog + // must recognise the group and validate the option name after the third dot. + String text = "camel.vault.aws.accessKey=AKIA1234"; + ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + assertTrue(result.isAccepted()); + + text = "camel.vault.aws.region=us-east-1"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + assertTrue(result.isAccepted()); + + // invented option under the nested group must be accepted (group is known) but invalid + text = "camel.vault.aws.inventedOption=foo"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertTrue(result.isAccepted()); + assertTrue(result.getUnknown().contains("camel.vault.aws.inventedOption")); + } + + @Test + public void testValidateConfigurationPropertyDashedGroupName() { + // The metadata group is "camel.errorRegistry" — the dashed form + // "camel.error-registry" must be accepted and validated. + String text = "camel.error-registry.enabled=true"; + ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + assertTrue(result.isAccepted()); + + text = "camel.error-registry.maximumEntries=100"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + assertTrue(result.isAccepted()); + + text = "camel.ai-observability.enabled=true"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + assertTrue(result.isAccepted()); + } + + @Test + public void testValidateConfigurationPropertyServerInventedOption() { + // camel.server.* is a known group — a real option must pass, + // and an invented option must fail validation. + String text = "camel.server.port=9999"; + ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + assertTrue(result.isAccepted()); + + text = "camel.server.inventedOption=foo"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertTrue(result.isAccepted()); + assertTrue(result.getUnknown().contains("camel.server.inventedOption")); + } + @Test public void validateEnvVariableInSyntax() { EndpointValidationResult result From 67b637b1065ca7ce4b36de8da8ba4805014df31d Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 08:20:18 +0000 Subject: [PATCH 08/16] test: add springBootStarterOnlyOptionIsUnchecked test --- .../mcp/ConfigurationValidateToolsTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java index 7296fdb53654f..c8a736aad4701 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java @@ -296,6 +296,24 @@ void dashedGroupNameIsCheckedAndValid() { assertThat(info.valid()).isTrue(); } + @Test + void springBootStarterOnlyOptionIsUnchecked() { + // camel.metrics.uriTagEnabled exists in camel-micrometer-starter but not in the + // camel-main metadata the catalog uses — it must not be reported as an error for + // runtime=spring-boot. + ConfigurationValidateTools tools = createTools(); + + ConfigurationValidateTools.ConfigurationValidateResult result + = tools.camel_configuration_validate( + "camel.metrics.uriTagEnabled=true", "spring-boot", null, null); + + ConfigurationValidateTools.PropertyValidationInfo info = result.properties().get(0); + assertThat(info.checked()).isFalse(); + assertThat(info.valid()).isFalse(); + assertThat(result.summary().unchecked()).isEqualTo(1); + assertThat(result.summary().errors()).isZero(); + } + @Test void blankInputThrows() { ConfigurationValidateTools tools = createTools(); From 24be01bbc7c7f6b6e98e00067efef1dc52b70fb4 Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 09:01:29 +0000 Subject: [PATCH 09/16] CAMEL-25538: strengthen springBootStarterOnlyOptionIsUnchecked test with counterpart assertion --- .../commands/mcp/ConfigurationValidateToolsTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java index c8a736aad4701..3689aa82e6a84 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java @@ -312,6 +312,17 @@ void springBootStarterOnlyOptionIsUnchecked() { assertThat(info.valid()).isFalse(); assertThat(result.summary().unchecked()).isEqualTo(1); assertThat(result.summary().errors()).isZero(); + + // Counterpart: without the spring-boot flag, camel.metrics.uriTagEnabled IS checked + // (camel.metrics is a known main-model group) but flagged as unknown option. + // This assertion FAILS on the old code (camel.metrics not in hardcoded prefix list) + // and confirms the dynamic group-matching path is exercised, not just the old fallback. + ConfigurationValidateTools.ConfigurationValidateResult nonSbResult + = tools.camel_configuration_validate( + "camel.metrics.uriTagEnabled=true", null, null, null); + ConfigurationValidateTools.PropertyValidationInfo nonSbInfo = nonSbResult.properties().get(0); + assertThat(nonSbInfo.checked()).isTrue(); // camel.metrics group is known + assertThat(nonSbInfo.valid()).isFalse(); // but uriTagEnabled is not in main metadata } @Test From 7e32ba878cd6b6747f2fef266305b9a43e1c8bed Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 10:05:04 +0000 Subject: [PATCH 10/16] fix: fall back to default catalog for spring-boot runtime when download fails When runtime=spring-boot with no explicit version or platformBom, CatalogService.loadCatalog() attempted to download the Spring Boot catalog provider from Maven Central. In CI environments without network access this raised a DownloadException, causing ConfigurationValidateTools tests to fail with a ToolCallException rather than the expected result. Extend the existing offline-fallback logic (already in place for main runtime + explicit version) to also cover spring-boot runtime without a pinned version. The default camel-main catalog is sufficient for configuration property validation in that case. Also add a dedicated CatalogServiceTest that reproduces the failure using a non-existent file: repository, which always triggers the download error without requiring network access. --- .../dsl/jbang/core/commands/mcp/CatalogService.java | 8 +++++--- .../jbang/core/commands/mcp/CatalogServiceTest.java | 12 ++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogService.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogService.java index 5594ce1276c88..65c780318be47 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogService.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogService.java @@ -144,9 +144,11 @@ public CamelCatalog loadCatalog(String runtime, String camelVersion, String plat try { loaded = doLoadCatalog(runtimeType, camelVersion, platformBomGav); } catch (Exception e) { - if (runtimeType == RuntimeType.main && hasVersion && !hasBom) { - // a version that cannot be downloaded (not released, no network): answer from the default catalog - // rather than fail the tool call + if (!hasBom && (runtimeType == RuntimeType.main && hasVersion + || runtimeType == RuntimeType.springBoot && !hasVersion)) { + // a version/artifact that cannot be downloaded (not released, no network): answer from the default + // catalog rather than fail the tool call. For spring-boot without a pinned version, the default + // camel-main catalog is close enough for property validation and avoids flaky network failures. return defaultCatalog; } throw e; diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogServiceTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogServiceTest.java index 164994d1c193c..a6714a548d12f 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogServiceTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/CatalogServiceTest.java @@ -152,6 +152,18 @@ void platformBomVersionTakesPrecedenceOverCamelVersion() { assertThat(version(createService(null), null, "9.99.99", bom)).isEqualTo(BUILTIN_VERSION); } + @Test + void springBootWithoutVersionFallsBackToDefaultCatalogWhenOffline() { + // When runtime=spring-boot with no explicit version or BOM, the catalog loader tries to download + // the Spring Boot catalog provider from Maven Central. In environments without network access (CI), + // this fails with a DownloadException. The fix is to fall back to the default catalog rather than + // surfacing a confusing network error to the user. + // This test uses a non-existent repo so the download always fails, simulating offline/CI behaviour. + CatalogService service = createService("file:///nonexistent-repo-that-never-exists"); + + assertThat(version(service, "spring-boot", null, null)).isEqualTo(BUILTIN_VERSION); + } + // Download tests — require Maven Central access. // Disabled in CI environments to avoid flaky network-dependent failures. From 7efd474c2f4ae4255c20fdfa324ab6ec04636205 Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 10:32:18 +0000 Subject: [PATCH 11/16] fix: matchMainModelGroup must not strip 'camel.' from group name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit normKey carries the full 'camel.*' key (e.g. 'camel.errorregistry.enabled'), but the loop was stripping 'camel.' from group.getName() before normalising, producing normGroupName = 'errorregistry'. The startsWith check then compared 'camel.errorregistry.enabled' against 'errorregistry.' which never matches. acceptConfigurationPropertyKey avoids the same pitfall by normalising the full group name ('camel.errorRegistry' -> 'camel.errorregistry') and comparing it against the full normKey, which is what matchMainModelGroup should do too. Effect of the bug: matchMainModelGroup always returned null, so the else-branch in validateConfigurationProperty never executed — unknown options under accepted main-model groups (e.g. camel.server.inventedOption) were silently validated with isSuccess()=true, making valid=true when they should be valid=false. --- .../catalog/impl/AbstractCamelCatalog.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java index 773152107856c..222e53e7abd0c 100644 --- a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java +++ b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java @@ -1451,28 +1451,27 @@ private boolean acceptConfigurationPropertyKey(String key, MainModel cachedModel } /** - * Returns the matched group from the main-model metadata when the key (with the leading {@code camel.} stripped) - * corresponds to one of the registered groups, or {@code null} if no group matches. + * Returns the matched group from the main-model metadata when the key (full {@code camel.*} key) corresponds to one + * of the registered groups, or {@code null} if no group matches. *

* Comparison is normalised (dashes removed, lower-cased) to mirror how camel-main binds keys at runtime via * {@code MainHelper.optionKey} and {@code startsWithIgnoreCase}. *

* Note: {@link #acceptConfigurationPropertyKey} also iterates the groups with the same normalisation, but it - * receives the full {@code camel.*} key (not the stripped form) and returns a boolean rather than the matched - * group. A direct delegation is awkward because the two callers need different key conventions, so both methods are - * kept separate and share the normalisation comment above. + * returns a boolean rather than the matched group. Both methods receive the full {@code camel.*} key and compare it + * against the full group name (including the {@code camel.} prefix). */ private MainModel.MainGroupModel matchMainModelGroup(String key, MainModel cachedModel) { MainModel model = cachedModel; if (model == null) { return null; } - // Normalise the incoming key once + // Normalise the incoming key once (full key, e.g. "camel.errorregistry.enabled") String normKey = key.replace("-", "").toLowerCase(Locale.ENGLISH); for (MainModel.MainGroupModel group : model.getGroups()) { - // group name is e.g. "camel.server" — strip the leading "camel." to get "server" - String groupName = group.getName().substring("camel.".length()); - String normGroupName = groupName.replace("-", "").toLowerCase(Locale.ENGLISH); + // group name is e.g. "camel.errorRegistry" — normalise the whole name (including "camel." prefix) + // to match the full key; do NOT strip "camel." here, because normKey still carries it. + String normGroupName = group.getName().replace("-", "").toLowerCase(Locale.ENGLISH); if (normKey.startsWith(normGroupName + ".")) { return group; } From 494e4cbbf0022167b05569bc3cc781085c7174f7 Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 11:09:11 +0000 Subject: [PATCH 12/16] =?UTF-8?q?fix:=20pass=20full=20key=20(longKey)=20to?= =?UTF-8?q?=20matchMainModelGroup=20=E2=80=94=20strip=20had=20already=20be?= =?UTF-8?q?en=20applied=20to=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/apache/camel/catalog/impl/AbstractCamelCatalog.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java index 222e53e7abd0c..b00450882a44e 100644 --- a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java +++ b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java @@ -1241,8 +1241,10 @@ public ConfigurationPropertiesValidationResult validateConfigurationProperty(Str validateConfigurationProperty(posDot, posBracket, suffix, nOption, result, rows, name, value, longKey); } } else { - // Check if the key matches a main-model group (dynamic — replaces the hardcoded list) - MainModel.MainGroupModel matchedGroup = matchMainModelGroup(key, cachedMainModel); + // Check if the key matches a main-model group (dynamic — replaces the hardcoded list). + // Must use longKey (the full camel.* key) because matchMainModelGroup compares against + // the full group names (e.g. "camel.server"), and key has already had "camel." stripped. + MainModel.MainGroupModel matchedGroup = matchMainModelGroup(longKey, cachedMainModel); if (matchedGroup != null) { String name = StringHelper.before(key, "."); if (value != null) { From 006252680bf1c2f2680d42752040ffcadde87771 Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 12:02:49 +0000 Subject: [PATCH 13/16] CAMEL-25538: narrow Spring Boot downgrade to newly-accepted groups; surface suggestions on downgrade; fix upgrade guide group count and unchecked description --- .../pages/camel-4x-upgrade-guide-4_24.adoc | 10 ++-- .../mcp/ConfigurationValidateTools.java | 54 +++++++++++++------ 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index bd9c27c6d523e..fbe719b68aada 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -18,9 +18,13 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] page for details. The catalog now accepts all `camel.*` groups defined in `camel-main-configuration-metadata.json` (previously only 10 groups out of 36 were recognized). As a result, `camel-report-maven-plugin:validate` and the camel-jbang MCP `camel_configuration_validate` tool -now validate approximately 26 additional `camel.*` groups (including `camel.server.*`, +now validate approximately 29 additional `camel.*` groups (including `camel.server.*`, `camel.management.*`, `camel.vault.*`, `camel.ssl.*`, and others). +If your project uses Spring Boot and has options such as `camel.metrics.uriTagEnabled` (from a +Camel starter) that are not in the camel-main schema, `camel-report-maven-plugin:validate` may +report them as "Unknown option". See below for details. + Group names are now matched with dash- and case-insensitive normalisation, matching how camel-main binds keys at runtime. For example, `camel.error-registry.enabled` and `camel.ai-observability.enabled` are now accepted alongside their camel-case equivalents. @@ -34,8 +38,8 @@ The `camel_configuration_validate` MCP tool response format has changed: * `PropertyValidationInfo.accepted` is renamed to `checked`. * `ConfigurationValidateSummary` gains a new `unchecked` integer field counting lines whose namespace is not recognized as a Camel property (e.g. `server.port=8080` or `quarkus.*` keys). - Such lines had no `accepted` flag before; they are now explicitly classified as `checked=false` - and excluded from `summary.valid` and `summary.errors`. + Before, such lines were returned with `accepted=false` and `valid=true`; they are now + `checked=false` and are left out of `summary.valid` and `summary.errors`. If you call `camel_configuration_validate` programmatically and read the JSON response, update field references from `accepted` to `checked` and handle the new `summary.unchecked` field. diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java index c31367c9bac87..e950894d1e544 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java @@ -139,29 +139,51 @@ private PropertyValidationInfo toPropertyValidationInfo( // For Spring Boot, the catalog uses camel-main metadata which does not include // starter-specific options (e.g. camel.metrics.uriTagEnabled from camel-micrometer-starter). - // When the only issues are "unknown option" errors, downgrade to unchecked to avoid - // false positives — the starter may legitimately bind options outside the main schema. + // When the only issues are "unknown option" errors AND the key belongs to a group that is + // newly accepted by this fix (not one of the original 10 hardcoded prefixes), downgrade to + // unchecked to avoid false positives — the starter may legitimately bind options outside the + // main schema. Keys under the original 10 groups (component, dataformat, language, main, + // resilience4j, faulttolerance, threadpool, health, lra, rest) retain full validation since + // the catalog has always covered them. + boolean springBootDowngraded = false; if (isSpringBoot && checked && !valid) { - Set unknownKeys = result.getUnknown() != null ? result.getUnknown() : Collections.emptySet(); - boolean onlyUnknownOptionErrors = !unknownKeys.isEmpty() - && result.getNumberOfErrors() == unknownKeys.size() - && result.getSyntaxError() == null - && result.getIncapable() == null - && result.getInvalidBoolean() == null - && result.getInvalidEnum() == null - && result.getInvalidInteger() == null - && result.getInvalidNumber() == null - && result.getInvalidDuration() == null; - if (onlyUnknownOptionErrors) { - checked = false; - valid = false; + String propertyKey = text != null && text.contains("=") ? text.substring(0, text.indexOf('=')).trim() : text; + boolean isOriginalGroup = propertyKey != null && ( + propertyKey.startsWith("camel.component.") + || propertyKey.startsWith("camel.dataformat.") + || propertyKey.startsWith("camel.language.") + || propertyKey.startsWith("camel.main.") + || propertyKey.startsWith("camel.resilience4j.") + || propertyKey.startsWith("camel.faulttolerance.") + || propertyKey.startsWith("camel.threadpool.") + || propertyKey.startsWith("camel.health.") + || propertyKey.startsWith("camel.lra.") + || propertyKey.startsWith("camel.rest.")); + if (!isOriginalGroup) { + Set unknownKeys = result.getUnknown() != null ? result.getUnknown() : Collections.emptySet(); + boolean onlyUnknownOptionErrors = !unknownKeys.isEmpty() + && result.getNumberOfErrors() == unknownKeys.size() + && result.getSyntaxError() == null + && result.getIncapable() == null + && result.getInvalidBoolean() == null + && result.getInvalidEnum() == null + && result.getInvalidInteger() == null + && result.getInvalidNumber() == null + && result.getInvalidDuration() == null; + if (onlyUnknownOptionErrors) { + checked = false; + valid = false; + springBootDowngraded = true; + } } } int errors = checked ? result.getNumberOfErrors() : 0; int warnings = checked ? result.getNumberOfWarnings() : 0; - List issues = checked ? collectIssues(result) : Collections.emptyList(); + // On a Spring Boot downgrade, still surface suggestions so the user can act on them + // even though the line is classified as unchecked (the count is excluded from summary.errors). + List issues = (checked || springBootDowngraded) ? collectIssues(result) : Collections.emptyList(); String summary = checked ? result.summaryErrorMessage(false, true, true) : null; return new PropertyValidationInfo(lineNumber, text, checked, valid, errors, warnings, summary, issues); From 507ce23562e32f510287126b6fc7e2b739672cf9 Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 12:15:43 +0000 Subject: [PATCH 14/16] fix: apply formatter changes to ConfigurationValidateTools --- .../jbang/core/commands/mcp/ConfigurationValidateTools.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java index e950894d1e544..7232577efac29 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java @@ -148,8 +148,7 @@ private PropertyValidationInfo toPropertyValidationInfo( boolean springBootDowngraded = false; if (isSpringBoot && checked && !valid) { String propertyKey = text != null && text.contains("=") ? text.substring(0, text.indexOf('=')).trim() : text; - boolean isOriginalGroup = propertyKey != null && ( - propertyKey.startsWith("camel.component.") + boolean isOriginalGroup = propertyKey != null && (propertyKey.startsWith("camel.component.") || propertyKey.startsWith("camel.dataformat.") || propertyKey.startsWith("camel.language.") || propertyKey.startsWith("camel.main.") From be91daafd2a68c1da0dea63cc98eebe5acfdb70b Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 12:21:45 +0000 Subject: [PATCH 15/16] fix: normalize isOriginalGroup key to handle Spring Boot relaxed-binding dashed forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spring Boot relaxed-binding writes 'camel.fault-tolerance.*' and 'camel.thread-pool.*' with dashes. The isOriginalGroup prefix checks used the raw key which would miss these dashed forms (e.g. 'camel.faulttolerance.' does not match 'camel.fault-tolerance.'). Normalize the key the same way the catalog does — remove dashes and lowercase — before the startsWith checks. --- .../mcp/ConfigurationValidateTools.java | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java index 7232577efac29..8855453bfd984 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java @@ -148,16 +148,20 @@ private PropertyValidationInfo toPropertyValidationInfo( boolean springBootDowngraded = false; if (isSpringBoot && checked && !valid) { String propertyKey = text != null && text.contains("=") ? text.substring(0, text.indexOf('=')).trim() : text; - boolean isOriginalGroup = propertyKey != null && (propertyKey.startsWith("camel.component.") - || propertyKey.startsWith("camel.dataformat.") - || propertyKey.startsWith("camel.language.") - || propertyKey.startsWith("camel.main.") - || propertyKey.startsWith("camel.resilience4j.") - || propertyKey.startsWith("camel.faulttolerance.") - || propertyKey.startsWith("camel.threadpool.") - || propertyKey.startsWith("camel.health.") - || propertyKey.startsWith("camel.lra.") - || propertyKey.startsWith("camel.rest.")); + // Normalize to dash-free lowercase so that Spring Boot relaxed-binding forms + // (e.g. camel.fault-tolerance.*, camel.thread-pool.*) match the original group + // prefixes the same way the catalog normalises them internally. + String normKey = propertyKey != null ? propertyKey.replace("-", "").toLowerCase(java.util.Locale.ENGLISH) : null; + boolean isOriginalGroup = normKey != null && (normKey.startsWith("camel.component.") + || normKey.startsWith("camel.dataformat.") + || normKey.startsWith("camel.language.") + || normKey.startsWith("camel.main.") + || normKey.startsWith("camel.resilience4j.") + || normKey.startsWith("camel.faulttolerance.") + || normKey.startsWith("camel.threadpool.") + || normKey.startsWith("camel.health.") + || normKey.startsWith("camel.lra.") + || normKey.startsWith("camel.rest.")); if (!isOriginalGroup) { Set unknownKeys = result.getUnknown() != null ? result.getUnknown() : Collections.emptySet(); boolean onlyUnknownOptionErrors = !unknownKeys.isEmpty() From e322e883835291c4f0be19d5516603a167e2fa4b Mon Sep 17 00:00:00 2001 From: gnodet Date: Fri, 16 Oct 2026 14:20:39 +0000 Subject: [PATCH 16/16] CAMEL-25538: address nits from davsclaus review - import java.util.Locale instead of FQCN (CLAUDE.md style) - springBootStarterOnlyOptionIsUnchecked: assert issues() contains unknown kind - add springBootKeepsValidationForOriginalGroups test covering component and fault-tolerance (dashed form) typos under spring-boot runtime - upgrade guide: fix group count (7 of 36, not 10), remove approximate qualifier, replace dangling 'See below' with concrete consequence sentence --- .../pages/camel-4x-upgrade-guide-4_24.adoc | 9 ++++--- .../mcp/ConfigurationValidateTools.java | 3 ++- .../mcp/ConfigurationValidateToolsTest.java | 26 +++++++++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index fbe719b68aada..6312433870f85 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -16,14 +16,17 @@ See the xref:camel-upgrade-recipes-tool.adoc[documentation] page for details. === camel-report-maven-plugin:validate / configuration-validate The catalog now accepts all `camel.*` groups defined in `camel-main-configuration-metadata.json` -(previously only 10 groups out of 36 were recognized). As a result, +(previously only 7 of the 36 groups were recognized, besides `camel.component.*`, `camel.dataformat.*` +and `camel.language.*`). As a result, `camel-report-maven-plugin:validate` and the camel-jbang MCP `camel_configuration_validate` tool -now validate approximately 29 additional `camel.*` groups (including `camel.server.*`, +now validate 29 additional `camel.*` groups (including `camel.server.*`, `camel.management.*`, `camel.vault.*`, `camel.ssl.*`, and others). If your project uses Spring Boot and has options such as `camel.metrics.uriTagEnabled` (from a Camel starter) that are not in the camel-main schema, `camel-report-maven-plugin:validate` may -report them as "Unknown option". See below for details. +report them as "Unknown option", which fails the build when `failOnError=true`. The +`camel_configuration_validate` MCP tool with `runtime=spring-boot` reports such keys as +`checked=false` instead. Group names are now matched with dash- and case-insensitive normalisation, matching how camel-main binds keys at runtime. For example, `camel.error-registry.enabled` and diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java index 8855453bfd984..f9d6075c53942 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateTools.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; @@ -151,7 +152,7 @@ private PropertyValidationInfo toPropertyValidationInfo( // Normalize to dash-free lowercase so that Spring Boot relaxed-binding forms // (e.g. camel.fault-tolerance.*, camel.thread-pool.*) match the original group // prefixes the same way the catalog normalises them internally. - String normKey = propertyKey != null ? propertyKey.replace("-", "").toLowerCase(java.util.Locale.ENGLISH) : null; + String normKey = propertyKey != null ? propertyKey.replace("-", "").toLowerCase(Locale.ENGLISH) : null; boolean isOriginalGroup = normKey != null && (normKey.startsWith("camel.component.") || normKey.startsWith("camel.dataformat.") || normKey.startsWith("camel.language.") diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java index 3689aa82e6a84..9de2372fa479d 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/ConfigurationValidateToolsTest.java @@ -312,6 +312,7 @@ void springBootStarterOnlyOptionIsUnchecked() { assertThat(info.valid()).isFalse(); assertThat(result.summary().unchecked()).isEqualTo(1); assertThat(result.summary().errors()).isZero(); + assertThat(info.issues()).anyMatch(i -> "unknown".equals(i.kind())); // Counterpart: without the spring-boot flag, camel.metrics.uriTagEnabled IS checked // (camel.metrics is a known main-model group) but flagged as unknown option. @@ -325,6 +326,31 @@ void springBootStarterOnlyOptionIsUnchecked() { assertThat(nonSbInfo.valid()).isFalse(); // but uriTagEnabled is not in main metadata } + @Test + void springBootKeepsValidationForOriginalGroups() { + // Original groups (camel.component.*, camel.fault-tolerance.*, etc.) must remain fully + // validated under Spring Boot — only newly-accepted dynamic groups are downgraded. + ConfigurationValidateTools tools = createTools(); + + // camel.component.kafka.brokerz is a typo — must still be checked and invalid with a suggestion + ConfigurationValidateTools.ConfigurationValidateResult componentResult + = tools.camel_configuration_validate( + "camel.component.kafka.brokerz=localhost:9092", "spring-boot", null, null); + ConfigurationValidateTools.PropertyValidationInfo componentInfo = componentResult.properties().get(0); + assertThat(componentInfo.checked()).isTrue(); + assertThat(componentInfo.valid()).isFalse(); + assertThat(componentInfo.issues()).anyMatch(i -> "unknown".equals(i.kind())); + + // camel.fault-tolerance.* (dashed form of camel.faulttolerance.*) must also stay validated + ConfigurationValidateTools.ConfigurationValidateResult ftResult + = tools.camel_configuration_validate( + "camel.fault-tolerance.timeoutEnabledz=true", "spring-boot", null, null); + ConfigurationValidateTools.PropertyValidationInfo ftInfo = ftResult.properties().get(0); + assertThat(ftInfo.checked()).isTrue(); + assertThat(ftInfo.valid()).isFalse(); + assertThat(ftInfo.issues()).anyMatch(i -> "unknown".equals(i.kind())); + } + @Test void blankInputThrows() { ConfigurationValidateTools tools = createTools();