This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 100afa698552 CAMEL-24856: the properties validator reports a nested
segment under an option group, which fails at startup
100afa698552 is described below
commit 100afa698552572d6a36a1e86999d54a8765a345
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 21 09:44:02 2026 +0200
CAMEL-24856: the properties validator reports a nested segment under an
option group, which fails at startup
camel.resilience4j.circuitbreaker.supplierCircuitBreaker.slidingWindowSize=4,
an invented per-id form, passed the properties validation and the run died
at
startup with "Cannot find getter method: supplierCircuitBreaker on bean:
class java.lang.String". The properties checks report a key with a nested
segment under one of the main model's option groups (resilience4j,
faulttolerance, threadpool, health...) with the global form and, for
resilience4j, the per circuit breaker form in the route. The prefixes whose
keys nest by design (camel.component, camel.beans, camel.variable,
camel.kamelet, camel.main, bracketed keys) are left to the checks that own
them.
Closes #26639
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
---
.../jbang/core/commands/ai/PropertiesChecks.java | 57 ++++++++++++++++++++++
.../commands/ai/SourceValidatorPropertiesTest.java | 19 ++++++++
2 files changed, 76 insertions(+)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/PropertiesChecks.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/PropertiesChecks.java
index 4ddc2265e610..52bf04e7f05f 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/PropertiesChecks.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/PropertiesChecks.java
@@ -21,12 +21,14 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.ConfigurationPropertiesValidationResult;
+import org.apache.camel.tooling.model.MainModel;
/**
* The application.properties checks of {@link SourceValidator}: unknown
camel.* options with the option meant, an
@@ -46,6 +48,56 @@ final class PropertiesChecks {
return validatePropertiesLines(content, line ->
validatePropertyLine(line, catalog, extraPropertyLine));
}
+ /** camel.<group>.<rest>=: the option groups of the main model
(resilience4j, faulttolerance, threadpool...). */
+ static final Pattern GROUP_KEY_PATTERN =
Pattern.compile("^\\s*camel\\.([a-zA-Z0-9-]+)\\.([^=\\s]+)\\s*=");
+
+ /** The camel.* prefixes whose keys legitimately nest, or that other
checks own. */
+ private static final Set<String> NESTING_GROUPS = Set.of("component",
"dataformat", "language", "beans", "variable",
+ "kamelet", "jbang", "route-template", "routeTemplate", "main",
"rest", "server", "management");
+
+ /**
+ *
camel.resilience4j.circuitbreaker.supplierCircuitBreaker.slidingWindowSize=4,
an invented per-id form: the
+ * catalog accepts it and the run dies at startup ("Cannot find getter
method: supplierCircuitBreaker on bean: class
+ * java.lang.String"). The options of a group are global, one segment
after the group; resilience4j is also set per
+ * circuit breaker in the route (CAMEL-24856).
+ */
+ static String nestedGroupKeyHint(String line, CamelCatalog catalog) {
+ Matcher m = GROUP_KEY_PATTERN.matcher(line);
+ if (!m.find()) {
+ return null;
+ }
+ String group = m.group(1);
+ String rest = m.group(2);
+ if (!rest.contains(".") || rest.contains("[") ||
NESTING_GROUPS.contains(group)) {
+ return null;
+ }
+ MainModel mm = catalog.mainModel();
+ if (mm == null) {
+ return null; // a catalog without the main model: the key goes to
the catalog as is
+ }
+ String prefix = "camel." + group + ".";
+ List<String> options = new ArrayList<>();
+ for (var o : mm.getOptions()) {
+ if (o.getName().startsWith(prefix) &&
!o.getName().substring(prefix.length()).contains(".")) {
+ options.add(o.getName().substring(prefix.length()));
+ }
+ }
+ if (options.isEmpty()) {
+ return null; // not a known group: the catalog reports the key
+ }
+ String first = rest.substring(0, rest.indexOf('.'));
+ String last = rest.substring(rest.lastIndexOf('.') + 1);
+ String option = options.contains(last) ? last : closestName(last,
options);
+ String example = "camel." + group + "." + (option != null ? option :
options.get(0)) + "=...";
+ String more = "resilience4j".equals(group)
+ ? ", or per circuit breaker in the route: circuitBreaker:
{resilience4jConfiguration: {"
+ + (option != null ? option : "...") + ": ...}}"
+ : "; the options are " + String.join(", ", options.size() > 8
? options.subList(0, 8) : options)
+ + (options.size() > 8 ? ", ..." : "");
+ return first + " Unknown option (camel." + group + " has no nested
settings such as " + first
+ + ": its options are global, " + example + more + ")";
+ }
+
/** Validates one properties line: a {@code camel.*} key against the
catalog, any other with the extra check. */
static final Pattern COMPONENT_KEY_PATTERN
=
Pattern.compile("^\\s*camel\\.(component|dataformat|language)\\.([A-Za-z0-9-]+)\\.");
@@ -76,6 +128,11 @@ final class PropertiesChecks {
return name + " Unknown " + kind + (closest != null ? "
(did you mean " + closest + "?)" : "");
}
}
+ // camel.resilience4j.circuitbreaker.<id>.<option>: a nested segment
under a known option group (CAMEL-24856)
+ String nested = nestedGroupKeyHint(line, catalog);
+ if (nested != null) {
+ return nested;
+ }
try {
ConfigurationPropertiesValidationResult result =
catalog.validateConfigurationProperty(line);
if (result.isAccepted()) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorPropertiesTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorPropertiesTest.java
index a9bff80561c0..b894b3ce1eb9 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorPropertiesTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorPropertiesTest.java
@@ -44,6 +44,25 @@ public class SourceValidatorPropertiesTest {
assertThat(msgs.get(1)).startsWith("Line 3: jacksn Unknown
dataformat").contains("did you mean jackson");
}
+ /**
+ * CAMEL-24856: a nested segment under an option group is not a property;
the options are global or in the route.
+ */
+ @Test
+ void aNestedKeyUnderAnOptionGroupIsReported() {
+ List<String> msgs = SourceValidator.validateProperties("""
+
camel.resilience4j.circuitbreaker.supplierCircuitBreaker.slidingWindowSize=4
+ camel.resilience4j.slidingWindowSize=4
+ camel.faulttolerance.bulkhead.myPool.enabled=true
+ """, catalog, null);
+ assertThat(msgs).hasSize(2);
+ assertThat(msgs.get(0))
+ .startsWith("Line 1: circuitbreaker Unknown option
(camel.resilience4j has no nested settings")
+ .contains("camel.resilience4j.slidingWindowSize=...")
+ .contains("circuitBreaker: {resilience4jConfiguration:
{slidingWindowSize: ...}}");
+ assertThat(msgs.get(1)).startsWith("Line 3: bulkhead Unknown option
(camel.faulttolerance has no nested settings")
+ .contains("the options are");
+ }
+
@Test
void wrongMainKeyGetsTheClosestOption() {
List<String> msgs = SourceValidator.validateProperties("""