Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
e4d6da5
CAMEL-25538: fix configuration-validate to accept all camel.* groups …
gnodet Oct 14, 2026
8966e92
CAMEL-25538: address review — checked/unchecked semantics, dash-insen…
Oct 16, 2026
83e0993
style: apply spotless formatting
gnodet Oct 16, 2026
319beb3
CAMEL-25538: address review — skip main-model load for component/data…
gnodet Oct 16, 2026
bb8df37
CAMEL-25538: address review — upgrade guide wire-format note, matchMa…
gnodet Oct 16, 2026
ce48265
CAMEL-25538: address review — remove dead model fallback, fix Spring …
gnodet Oct 16, 2026
bb0fea7
Merge apache/main into fix/camel-25538-fix-configuration-validate
gnodet Oct 16, 2026
6976c39
CAMEL-25538: add CamelCatalogTest direct tests for dynamic group matc…
gnodet Oct 16, 2026
67b637b
test: add springBootStarterOnlyOptionIsUnchecked test
gnodet Oct 16, 2026
24be01b
CAMEL-25538: strengthen springBootStarterOnlyOptionIsUnchecked test w…
gnodet Oct 16, 2026
7e32ba8
fix: fall back to default catalog for spring-boot runtime when downlo…
gnodet Oct 16, 2026
7efd474
fix: matchMainModelGroup must not strip 'camel.' from group name
gnodet Oct 16, 2026
494e4cb
fix: pass full key (longKey) to matchMainModelGroup — strip had alrea…
gnodet Oct 16, 2026
0062526
CAMEL-25538: narrow Spring Boot downgrade to newly-accepted groups; s…
gnodet Oct 16, 2026
507ce23
fix: apply formatter changes to ConfigurationValidateTools
gnodet Oct 16, 2026
be91daa
fix: normalize isOriginalGroup key to handle Spring Boot relaxed-bind…
gnodet Oct 16, 2026
e322e88
CAMEL-25538: address nits from davsclaus review
gnodet Oct 16, 2026
4d0f627
Merge upstream/main into fix/camel-25538-fix-configuration-validate
gnodet Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1185,7 +1185,15 @@ 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. 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);
return result;
Expand Down Expand Up @@ -1232,33 +1240,38 @@ 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<String, BaseOptionModel> 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).
// 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) {
// cachedMainModel is guaranteed non-null here: a match in matchMainModelGroup
// implies the model was available (the groups come from the model itself).
Map<String, BaseOptionModel> rows = new HashMap<>();
cachedMainModel.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);
}
}
}

Expand Down Expand Up @@ -1408,20 +1421,64 @@ private static boolean isValidSuffix(String suffix) {
return suffix == null || suffix.isEmpty() || suffix.equals(".");
}

private static boolean acceptConfigurationPropertyKey(String key) {
private boolean acceptConfigurationPropertyKey(String key, MainModel cachedModel) {
if (key == null) {
return false;
}
return key.startsWith("camel.component.")
// 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.")
|| 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.
// 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;
if (model != null) {
for (MainModel.MainGroupModel group : model.getGroups()) {
String normGroup = group.getName().replace("-", "").toLowerCase(Locale.ENGLISH) + ".";
if (normKey.startsWith(normGroup)) {
return true;
}
}
}
return false;
}

/**
* 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.
* <p>
* Comparison is normalised (dashes removed, lower-cased) to mirror how camel-main binds keys at runtime via
* {@code MainHelper.optionKey} and {@code startsWithIgnoreCase}.
* <p>
* Note: {@link #acceptConfigurationPropertyKey} also iterates the groups with the same normalisation, but it
* 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 (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.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;
}
}
return null;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,40 @@ in its model, so clearing them on the route saved little memory at the cost of m
Applications that called `Route.clearRouteModel()` directly can remove the call. Implementations of the `Route` interface
that override `clearRouteModel()` can remove the override.

=== camel-report-maven-plugin:validate / configuration-validate

The catalog now accepts all `camel.*` groups defined in `camel-main-configuration-metadata.json`
(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 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", 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
`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).
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.

=== camel-semantic - expert artifacts

TypeSafe AI Semantic applications must add `org.apache.camel:camel-semantic-typesafe-ai`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading