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 9a77f79176fa CAMEL-26316: camel-jbang - AI tools: circuit breaker
options in the route, a top-level ternary rewritten inside ${ }, and the cause
of a sql error (#28498)
9a77f79176fa is described below
commit 9a77f79176facc403e86f95e8540f8bc733225e8
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 26 20:41:08 2026 +0100
CAMEL-26316: camel-jbang - AI tools: circuit breaker options in the route,
a top-level ternary rewritten inside ${ }, and the cause of a sql error (#28498)
From the local-model benchmark of Oct 25-26 2026. camel_catalog_doc for
resilience4j/faulttolerance points to the in-route circuit breaker
configuration; a top-level ternary is shown rewritten inside one function; the
validator reports :#{...} in sql URIs; a reload ERROR line ends with the root
cause of its stack trace; camel_error_diagnose knows BadSqlGrammarException;
the errorHandler example fails a message with a filter and throwException.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../modules/ROOT/pages/error-handler.adoc | 22 +++++-
.../dsl/jbang/core/commands/ai/eip-samples.json | 2 +-
.../dsl/jbang/core/commands/ai/CatalogDocs.java | 29 ++++++-
.../dsl/jbang/core/commands/ai/DiagnoseData.java | 22 ++++++
.../dsl/jbang/core/commands/ai/EndpointChecks.java | 27 +++++++
.../dsl/jbang/core/commands/ai/ReloadOutcome.java | 39 ++++++++++
.../dsl/jbang/core/commands/ai/SimpleChecks.java | 90 +++++++++++++++++++++-
.../jbang/core/commands/ai/CatalogDocsTest.java | 17 ++++
.../jbang/core/commands/ai/ReloadOutcomeTest.java | 35 +++++++++
.../commands/ai/SourceValidatorSimpleTest.java | 44 ++++++++++-
.../ai/SourceValidatorSqlParametersTest.java | 37 +++++++++
11 files changed, 352 insertions(+), 12 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/error-handler.adoc
b/docs/user-manual/modules/ROOT/pages/error-handler.adoc
index b5af1a4d6beb..3fbf8ef01979 100644
--- a/docs/user-manual/modules/ROOT/pages/error-handler.adoc
+++ b/docs/user-manual/modules/ROOT/pages/error-handler.adoc
@@ -249,6 +249,10 @@ In this example, we configure a
xref:components:eips:dead-letter-channel.adoc[De
Channel] on the route that should redeliver at most 3 times and use a
little delay before retrying.
+The route fails on purpose when an order is not reserved: a
xref:components:eips:filter-eip.adoc[Filter]
+with xref:components:eips:throwException-eip.adoc[Throw Exception] in it.
After the redeliveries,
+the order goes to the dead letter endpoint. Any exception does the same, such
as one that a bean or a processor throws.
+
[tabs]
====
@@ -263,7 +267,10 @@ XML::
</deadLetterChannel>
</errorHandler>
<from uri="direct:in"/>
- <process ref="myFailureProcessor"/>
+ <filter>
+ <simple>${header.status} != 'reserved'</simple>
+ <throwException exceptionType="java.lang.IllegalStateException"
message="Order ${header.orderId} is not reserved"/>
+ </filter>
<to uri="mock:result"/>
</route>
----
@@ -302,7 +309,10 @@ etc.
<!-- set the errorHandlerRef to our DeadLetterChannel, this applies
for this route only -->
<route errorHandlerRef="myDeadLetterErrorHandler">
<from uri="direct:in"/>
- <process ref="myFailureProcessor"/>
+ <filter>
+ <simple>${header.status} != 'reserved'</simple>
+ <throwException
exceptionType="java.lang.IllegalStateException" message="Order
${header.orderId} is not reserved"/>
+ </filter>
<to uri="mock:result"/>
</route>
</camelContext>
@@ -322,8 +332,12 @@ YAML::
from:
uri: direct:in
steps:
- - process:
- ref: myFailureProcessor
+ - filter:
+ simple: "${header.status} != 'reserved'"
+ steps:
+ - throwException:
+ exceptionType: java.lang.IllegalStateException
+ message: "Order ${header.orderId} is not reserved"
- to:
uri: mock:result
----
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
index f4aa3c78cb1a..42e193f5a7b0 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
@@ -384,7 +384,7 @@
"errorHandler": [
{
"source": "error-handler.adoc",
- "yaml": "- route:\n errorHandler:\n deadLetterChannel:\n
deadLetterUri: mock:dead\n redeliveryPolicy:\n
maximumRedeliveries: 3\n redeliveryDelay: 250\n from:\n uri:
direct:in\n steps:\n - process:\n ref:
myFailureProcessor\n - to:\n uri: mock:result\n"
+ "yaml": "- route:\n errorHandler:\n deadLetterChannel:\n
deadLetterUri: mock:dead\n redeliveryPolicy:\n
maximumRedeliveries: 3\n redeliveryDelay: 250\n from:\n uri:
direct:in\n steps:\n - filter:\n simple:
\"${header.status} != 'reserved'\"\n steps:\n -
throwException:\n exceptionType:
java.lang.IllegalStateException\n message: \"Order
${header.orderId} i [...]
},
{
"source": "error-handler.adoc",
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
index 9fee53d39d85..bbbd27bb3932 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
@@ -914,10 +914,19 @@ public final class CatalogDocs {
return type.equals(element.getJavaType());
}
+ /**
+ * The main option groups of a circuit breaker implementation, with the
option of the circuitBreaker EIP that
+ * configures one circuit breaker in the route.
+ */
+ static final Map<String, String> CIRCUIT_BREAKER_CONFIGURATIONS = Map.of(
+ "camel.resilience4j", "resilience4jConfiguration",
+ "camel.faulttolerance", "faultToleranceConfiguration");
+
/**
* The camel.<name>.* main options when the name is one of their
groups (resilience4j, faulttolerance, health,
* metrics, threadpool, rest, ...), else null. A model asks for
"resilience4j" as a component when it wants the
- * application.properties keys of the circuit breaker.
+ * application.properties keys of the circuit breaker, or when it wants to
configure one circuit breaker, which is
+ * done in the route: the answer says so first.
*/
static JsonObject mainOptionsGroup(CamelCatalog catalog, String name) {
String n = name.trim().toLowerCase(Locale.ROOT);
@@ -978,11 +987,25 @@ public final class CatalogDocs {
result.put("note", "the " + group + ".* keys go in
application.properties and are read by camel run and"
+ " camel export, not by the Camel application
itself");
} else if (matched != null) {
+ // a reload applies the defaults of the circuit breakers
(CAMEL-26320), not the other main options
result.put("note", matched.getName() + " is a key for
application.properties (camel run and Camel Main read"
- + " it at startup); the other " + group + ".*
keys follow it");
+ + " it at startup" +
(CIRCUIT_BREAKER_CONFIGURATIONS.containsKey(group)
+ ? "" : ", so a reload of the running
integration does not apply it")
+ + "); the other " + group + ".* keys follow
it");
+ } else if (CIRCUIT_BREAKER_CONFIGURATIONS.containsKey(group)) {
+ // asked for "resilience4j" as a component to "add a
resilience4jConfiguration", a model wrote these keys to
+ // application.properties instead of configuring the circuit
breaker of its route (4 of 7 runs of a step in
+ // the local-model benchmark)
+ String eipOption = CIRCUIT_BREAKER_CONFIGURATIONS.get(group);
+ result.put("note", name + " is not a component: it runs the
circuitBreaker EIP. To configure a circuit breaker,"
+ + " set its options in the route, under
circuitBreaker: " + eipOption + ": (camel_catalog_doc"
+ + " name=" + eipOption + " kind=eip has them).
The " + group + ".* keys below are the"
+ + " defaults of every circuit breaker, for
application.properties");
+ result.put("inTheRoute", "circuitBreaker: {" + eipOption + ":
{...}}");
} else {
result.put("note", name + " is not a component: these are the " +
group + ".* keys for"
- + " application.properties (camel run reads
them at startup)");
+ + " application.properties (camel run reads
them at startup, so a reload of the running"
+ + " integration does not apply them)");
}
if (matched != null) {
result.put("matchedKey", matched.getName());
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DiagnoseData.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DiagnoseData.java
index c2189f079303..7d718b94c253 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DiagnoseData.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DiagnoseData.java
@@ -390,6 +390,28 @@ public class DiagnoseData {
List.of(
CAMEL_MANUAL_DOC + "security.html")));
+ // the sql component runs its statements with Spring JDBC: "bad SQL
grammar []" alone was diagnosed as no known
+ // exception, and the database's own message was only in the stack
trace (the local-model benchmark)
+ exceptions.put("BadSqlGrammarException", new ExceptionInfo(
+ "The database refused the SQL statement (Spring JDBC, which
the sql component uses). The database's own"
+ + " message
is in the cause: the Caused by line"
+ + " of the
stack trace (camel_get_log) names the"
+ + " error
and its position in the statement.",
+ List.of(
+ "A parameter in a form the sql component does not
read, such as :#{body.customer}: it reads"
+ + " :#name (a key of the body when the body is a Map,
or a header) and :#${...} (a simple"
+ + " expression), and sends anything else to the
database as written",
+ "An insert-or-update in the dialect of another
database (ON DUPLICATE KEY UPDATE is MySQL,"
+
+ " ON CONFLICT ... DO UPDATE is PostgreSQL)",
+ "A table or column that does not exist, or a typo in a
keyword",
+ "A parameter where SQL allows none, such as a table or
column name"),
+ List.of(
+ "Read the Caused by line in camel_get_log: it has the
database's message and the position",
+ "Write parameters as :#${body[customer]} or
:#customer",
+ "camel_runtime_sql_trace shows the statements as the
application sent them"),
+ List.of(
+ CAMEL_COMPONENT_DOC + "sql-component.html")));
+
KNOWN_EXCEPTIONS = Collections.unmodifiableMap(exceptions);
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
index 2d5410a8a8ee..6924f9fe6cbe 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
@@ -492,6 +492,14 @@ final class EndpointChecks {
/** :#excluded.country, :#EXCLUDED.country: the row being inserted written
as a named parameter. */
private static final Pattern EXCLUDED_PARAMETER =
Pattern.compile("(?i):#(excluded)\\.(\\w+)");
+ /**
+ * :#{body.customer}, the expression form of other frameworks: camel-sql
reads :#name and :#${simple}, so the JDBC
+ * driver gets it as written. Three runs of a step in the local-model
benchmark wrote it, the answer said only "bad
+ * SQL grammar []", and they fixed it after 69 to 92 failed messages.
+ */
+ private static final Pattern BRACE_PARAMETER =
Pattern.compile(":#\\{([^}]*)}");
+ private static final Pattern BODY_FIELD =
Pattern.compile("^body\\.(\\w+)$");
+
static void checkSqlNamedParameters(
List<String> errors, String fullUri, String rawLine, int
uriLineIdx, Map<String, Integer> optionLineMap) {
int colon = fullUri.indexOf(':');
@@ -549,6 +557,25 @@ final class EndpointChecks {
+ " being inserted in ON CONFLICT ... DO UPDATE, write
EXCLUDED." + first + " without :#"
+ (others.isEmpty() ? "" : " (also " + String.join(",
", others) + ")"));
}
+ Matcher brace = BRACE_PARAMETER.matcher(query);
+ List<String> braced = new ArrayList<>();
+ while (brace.find()) {
+ if (!braced.contains(brace.group())) {
+ braced.add(brace.group());
+ }
+ }
+ if (!braced.isEmpty()) {
+ Matcher inner = BRACE_PARAMETER.matcher(braced.get(0));
+ String expression = inner.matches() ? inner.group(1).strip() : "";
+ Matcher field = BODY_FIELD.matcher(expression);
+ String fix = field.matches()
+ ? ":#${body[" + field.group(1) + "]} (a Simple expression)
or :#" + field.group(1)
+ + " for a header or a key of a Map body"
+ : ":#${" + expression + "} (a Simple expression) or :#name
for a header or a key of a Map body";
+ errors.add(linePrefix(line) + "sql: " + braced.get(0) + " is not a
camel-sql named parameter (the JDBC driver"
+ + " gets it as written and fails with a syntax error):
write " + fix
+ + (braced.size() > 1 ? " (also " + String.join(", ",
braced.subList(1, braced.size())) + ")" : ""));
+ }
Matcher m = BARE_NAMED_PARAMETER.matcher(query);
List<String> bare = new ArrayList<>();
while (m.find()) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
index 13df72e1dfcb..89b048014e46 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
@@ -252,6 +252,42 @@ public final class ReloadOutcome {
return sb.toString();
}
+ private static final String CAUSED_BY = "Caused by: ";
+ private static final int MAX_CAUSE = 300;
+
+ /**
+ * The line with the root cause of its stack trace when the line does not
say it already: the last "Caused by" and
+ * the lines that continue its message (PostgreSQL gives the position of
the error on the next line). A sql: route
+ * failed with "BadSqlGrammarException: PreparedStatementCallback; bad SQL
grammar []", and the database's message
+ * ("ERROR: syntax error at or near ":"") was only in the stack trace (the
local-model benchmark).
+ */
+ static String withRootCause(String line, String detail) {
+ int at = detail != null ? detail.lastIndexOf(CAUSED_BY) : -1;
+ if (at < 0) {
+ return line;
+ }
+ String[] lines = detail.substring(at + CAUSED_BY.length()).split("\n");
+ StringBuilder cause = new StringBuilder(lines[0].strip());
+ for (int i = 1; i < lines.length && i < 3; i++) {
+ String next = lines[i].strip();
+ if (next.isEmpty() || next.startsWith("at ") ||
next.startsWith("... ")) {
+ break;
+ }
+ cause.append(' ').append(next);
+ }
+ String text = compact(cause.toString());
+ // the message of the cause, without its class: the line may have it
already (a "due to" of Camel)
+ int colon = text.indexOf(": ");
+ String message = colon > 0 ? text.substring(colon + 2) : text;
+ if (message.isBlank() || line.contains(message)) {
+ return line;
+ }
+ if (text.length() > MAX_CAUSE) {
+ text = text.substring(0, MAX_CAUSE) + "...";
+ }
+ return line + " (caused by " + text + ")";
+ }
+
static JsonArray output(List<JsonObject> newestFirst) {
List<String> lines = new ArrayList<>();
for (JsonObject r : newestFirst) {
@@ -274,6 +310,9 @@ public final class ReloadOutcome {
int max = OUTPUT_CHARS;
if (!routeLog) {
m = compact(m);
+ if ("ERROR".equalsIgnoreCase(level)) {
+ m = withRootCause(m, detail);
+ }
max = OUTPUT_ERROR_CHARS;
}
lines.add((routeLog ? "" : level.toUpperCase(Locale.ROOT) + " ")
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
index d312ac576205..e5ee4d6e3bdd 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
@@ -18,6 +18,8 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
import java.util.ArrayList;
import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.LanguageValidationResult;
@@ -184,12 +186,96 @@ final class SimpleChecks {
return null;
}
int question = indexOfTopLevel(text, " ? ", 0);
- if (question < 0 || indexOfTopLevel(text, " : ", question + 3) < 0) {
+ int colon = question < 0 ? -1 : indexOfTopLevel(text, " : ", question
+ 3);
+ if (colon < 0) {
return null;
}
+ // the expression itself, rewritten: with only an example, models
wrote the same top-level ternary up to 20
+ // times in a row (a step of the local-model benchmark)
+ String rewritten = ternaryInsideOneFunction(text, question, colon);
return "Simple has no top-level ternary: the ? at index " + (question
+ 1)
+ " and its : are outside ${...}, so they are literal text and
the operator is never evaluated."
- + " Write the whole ternary inside one function, as in
${body.size() == 0 ? ${null} : ${body[0]}}";
+ + (rewritten != null
+ ? " Write the whole ternary inside one function: " +
rewritten
+ : " Write the whole ternary inside one function, as in
${body.size() == 0 ? ${null} : ${body[0]}}");
+ }
+
+ /** A binary operator of Simple at the end of the text, after a function:
the function is part of the condition. */
+ private static final Pattern OPERATOR_AFTER_A_FUNCTION = Pattern.compile(
+
"}\\s*(?:==|!=|>=|<=|>|<|=~|!=~|contains|!contains|~~|regex|!regex|in|!in|range|!range|startsWith|endsWith"
+ +
"|is|!is)\\s*$");
+
+ /**
+ * The top-level ternary written inside one function, or null when no
function comes before the <tt>?</tt>: the
+ * function the condition starts with is opened before the condition and
closed after the second value, and a value
+ * written as bare text is quoted.
<tt>resource:file:${exchangeProperty.CamelTimerCounter} == 1 ? order.json :
+ * order-bad-email.json</tt> is
<tt>resource:file:${exchangeProperty.CamelTimerCounter == 1 ? 'order.json' :
+ * 'order-bad-email.json'}</tt>.
+ */
+ static String ternaryInsideOneFunction(String text, int question, int
colon) {
+ // the functions at the top level before the ?, as {start, end} with
end the index of the closing brace
+ List<int[]> functions = new ArrayList<>();
+ for (int i = 0; i < question; i++) {
+ if (text.charAt(i) == '$' && i < text.length() - 1 &&
text.charAt(i + 1) == '{') {
+ int close = closingBrace(text, i);
+ if (close < 0 || close > question) {
+ return null;
+ }
+ functions.add(new int[] { i, close });
+ i = close;
+ }
+ }
+ if (functions.isEmpty()) {
+ return null;
+ }
+ // the last function before the ?, or for ${a} == ${b} ? x : y the
function before the operator
+ int index = functions.size() - 1;
+ int[] last = functions.get(index);
+ Matcher op = OPERATOR_AFTER_A_FUNCTION.matcher(text.substring(0,
last[0]));
+ if (index > 0 && op.find() && op.start() == functions.get(index -
1)[1]) {
+ index--;
+ }
+ int start = functions.get(index)[0];
+ int end = functions.get(index)[1];
+ String condition = text.substring(start + 2, end) + text.substring(end
+ 1, question);
+ return text.substring(0, start) + "${" + condition + " ? " +
quoted(text.substring(question + 3, colon).trim())
+ + " : " + quoted(text.substring(colon + 3).trim()) + "}";
+ }
+
+ /** The index of the brace that closes the function opened at the index,
or -1. */
+ private static int closingBrace(String text, int open) {
+ char quote = 0;
+ int depth = 0;
+ for (int i = open; i < text.length(); i++) {
+ char ch = text.charAt(i);
+ if (quote != 0) {
+ if (ch == quote) {
+ quote = 0;
+ }
+ } else if (ch == '\'' || ch == '"') {
+ quote = ch;
+ } else if (ch == '$' && i < text.length() - 1 && text.charAt(i +
1) == '{') {
+ depth++;
+ i++;
+ } else if (ch == '{') {
+ // a brace of the function's own text, such as a closure
${body.find { it.x }}
+ depth++;
+ } else if (ch == '}' && --depth == 0) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * A value of the ternary: bare text is quoted, a quoted literal, a
function, a number, true, false and null not.
+ */
+ private static String quoted(String value) {
+ if (value.isEmpty() || value.startsWith("'") || value.startsWith("\"")
|| value.startsWith("${")
+ || value.matches("-?\\d+(\\.\\d+)?|true|false|null") ||
value.contains("'")) {
+ return value;
+ }
+ return "'" + value + "'";
}
/** The index of the token at the top level of the text: outside every
${...} and outside every quoted literal. */
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
index 2dcacc3ef74b..ab7d10e13e1a 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
@@ -543,6 +543,23 @@ class CatalogDocsTest {
assertEquals("camel.resilience4j", o.getString("group"));
assertTrue(o.getString("note").contains("not a component"),
o.getString("note"));
assertTrue(json.contains("camel.resilience4j.failureRateThreshold"),
json);
+ // the route first: asked this way to "add a
resilience4jConfiguration", a model wrote the keys to
+ // application.properties instead of configuring the circuit breaker
of its route (the local-model benchmark)
+ String note = o.getString("note");
+ assertTrue(note.contains("set its options in the route, under
circuitBreaker: resilience4jConfiguration:"), note);
+ assertTrue(note.indexOf("circuitBreaker:") <
note.indexOf("camel.resilience4j.*"), note);
+ // a reload applies the defaults of the circuit breakers (CAMEL-26320)
+ assertFalse(note.contains("does not apply"), note);
+ assertFalse(catalogDoc(Map.of("name",
"camel.resilience4j.slidingWindowSize")).getString("note")
+ .contains("does not apply"));
+ assertEquals("circuitBreaker: {resilience4jConfiguration: {...}}",
o.getString("inTheRoute"));
+ JsonObject faultTolerance = catalogDoc(Map.of("name",
"faulttolerance", "kind", "component"));
+ assertEquals("circuitBreaker: {faultToleranceConfiguration: {...}}",
faultTolerance.getString("inTheRoute"));
+ // a group without a route option says only where its keys go, and
that a reload does not apply them
+ JsonObject threadPool = catalogDoc(Map.of("name", "threadpool",
"kind", "component"));
+ assertEquals("camel.threadpool", threadPool.getString("group"));
+ assertNull(threadPool.get("inTheRoute"));
+ assertTrue(threadPool.getString("note").contains("does not apply
them"), threadPool.getString("note"));
// no kind: the same answer
args.remove("kind");
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
index befcec4ff7af..44e1d948a569 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
@@ -188,6 +188,41 @@ class ReloadOutcomeTest {
assertTrue(!line.contains("MessageId") && !line.contains("Exhausted
after"), line);
}
+ /**
+ * A sql: route failed with "bad SQL grammar []", and the database's
message was only in the stack trace (the
+ * local-model benchmark): the line ends with the root cause, with the
position PostgreSQL gives on its next line.
+ */
+ @Test
+ void theRootCauseOfAFailedExchangeIsAddedWhenTheLineDoesNotSayIt() {
+ List<String> failed = List.of(
+ "2026-10-25 22:20:53.000 INFO 42 --- [rReloadStrategy]
org.apache.camel.support.RouteWatcherReloadStrategy : Routes reloaded summary
(total:2 started:2 changed:1)",
+ "2026-10-25 22:20:54.029 ERROR 42 --- [- file://orders]
or.errorhandler.DefaultErrorHandler : Failed delivery for"
+
+ " (MessageId: D53CDBC0CA79126-02 on ExchangeId:
D53CDBC0CA79126-02) at"
+
+ " register-customers[to3] sql.camel.yaml:29. Exhausted
after delivery attempt: 1"
+
+ " caught:
org.springframework.jdbc.BadSqlGrammarException:"
+
+ " PreparedStatementCallback; bad SQL grammar []",
+ "",
+ "org.springframework.jdbc.BadSqlGrammarException:
PreparedStatementCallback; bad SQL grammar []",
+ "\tat
org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:112)",
+ "Caused by: org.postgresql.util.PSQLException: ERROR: syntax
error at or near \":\"",
+ " Position: 53",
+ "\tat
org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2993)");
+
+ JsonArray output = ReloadOutcome.output(records(BEFORE, failed));
+
+ assertEquals(1, output.size(), output.toJson());
+ assertEquals("ERROR Failed at register-customers[to3]
sql.camel.yaml:29: BadSqlGrammarException:"
+ + " PreparedStatementCallback; bad SQL grammar [] (caused
by PSQLException: ERROR: syntax error at or"
+ + " near \":\" Position: 53)",
+ output.get(0).toString());
+
+ // a cause the line already says is not repeated
+ assertEquals("Failed: X due to: IllegalStateException: no stock",
+ ReloadOutcome.withRootCause("Failed: X due to:
IllegalStateException: no stock",
+ "Caused by: java.lang.IllegalStateException: no
stock\n\tat a.B.c(B.java:1)"));
+ assertEquals("Failed: X", ReloadOutcome.withRootCause("Failed: X", "no
stack trace"));
+ }
+
@Test
void theDumpOfAnExchangeAndOfALongBeanGo() {
String m = ReloadOutcome.compact("Method with name: orderId not found
on bean: ["
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
index a2365b99a20a..74a7261735b7 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
@@ -431,7 +431,46 @@ class SourceValidatorSimpleTest {
assertThat(msgs.get(0)).startsWith("Line 5:")
.contains("Simple has no top-level ternary")
.contains("the ? at index 20")
- .contains("inside one function");
+ .endsWith("Write the whole ternary inside one function:
${body.size() == 0 ? ${null} : ${body[0]}}");
+ }
+
+ /**
+ * The message shows the expression itself rewritten: with only an
example, models wrote the same top-level ternary
+ * up to 20 times in a row (a step of the local-model benchmark).
+ */
+ @Test
+ void aTopLevelTernaryIsShownInsideOneFunction() {
+ assertThat(rewritten("${header.CamelTimerCounter} == 1 ? 'order.json'
: 'order-bad-email.json'"))
+ .isEqualTo("${header.CamelTimerCounter == 1 ? 'order.json' :
'order-bad-email.json'}");
+ // the literal text before the condition stays, and a bare value is
quoted
+
assertThat(rewritten("resource:file:${exchangeProperty.CamelTimerCounter} == 1
? order.json : order-bad-email.json"))
+ .isEqualTo("resource:file:${exchangeProperty.CamelTimerCounter
== 1 ? 'order.json' : 'order-bad-email.json'}");
+ assertThat(rewritten("Order ${header.id}: ${header.qty} > 5 ? big :
small"))
+ .isEqualTo("Order ${header.id}: ${header.qty > 5 ? 'big' :
'small'}");
+ // a brace of the function's own text does not end it
+ assertThat(rewritten("${body.find { it.x == 1 }} != null ? found :
none"))
+ .isEqualTo("${body.find { it.x == 1 } != null ? 'found' :
'none'}");
+ // two functions compared: the condition starts with the first of them
+ assertThat(rewritten("${header.a} == ${header.b} ? same : 2"))
+ .isEqualTo("${header.a == ${header.b} ? 'same' : 2}");
+ // no function before the ?: no rewrite, the message keeps its example
+ assertThat(SimpleChecks.topLevelTernary("Total 5 > 3 ? ${header.a} :
no"))
+ .endsWith("as in ${body.size() == 0 ? ${null} : ${body[0]}}");
+ // the rewritten expression is accepted
+ assertThat(SourceValidator.validateYamlSimple("""
+ - from:
+ uri: timer:tick
+ steps:
+ - setBody:
+ simple: "%s"
+ """.formatted(rewritten("${header.CamelTimerCounter} == 1 ?
'order.json' : 'order-bad-email.json'")),
+ catalog)).isEmpty();
+ }
+
+ private static String rewritten(String text) {
+ String message = SimpleChecks.topLevelTernary(text);
+ assertThat(message).contains("Write the whole ternary inside one
function: ");
+ return message.substring(message.indexOf("inside one function: ") +
"inside one function: ".length());
}
@Test
@@ -446,7 +485,8 @@ class SourceValidatorSimpleTest {
simple:
"resource:file:${exchangeProperty.CamelTimerCounter} == 1 ? order.json :
order-bad-email.json"
""", catalog);
assertThat(msgs).hasSize(1);
- assertThat(msgs.get(0)).startsWith("Line 5:").contains("Simple has no
top-level ternary");
+ assertThat(msgs.get(0)).startsWith("Line 5:").contains("Simple has no
top-level ternary")
+ .endsWith("resource:file:${exchangeProperty.CamelTimerCounter
== 1 ? 'order.json' : 'order-bad-email.json'}");
}
@Test
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSqlParametersTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSqlParametersTest.java
index 6aac64e0b9e4..85cb897b7fe4 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSqlParametersTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSqlParametersTest.java
@@ -65,6 +65,43 @@ class SourceValidatorSqlParametersTest {
&& e.contains("write :#${body[customer]} (a Simple
expression)"));
}
+ /**
+ * :#{body.customer} goes to the JDBC driver as written: three runs of a
step in the local-model benchmark wrote it,
+ * and the runtime said only "bad SQL grammar []".
+ */
+ @Test
+ void theBraceFormOfOtherFrameworksGetsTheSimpleForm() {
+ String yaml
+ = """
+ - route:
+ from:
+ uri: file:orders
+ steps:
+ - to:
+ uri: "sql:INSERT INTO customers (id,
country, orders) VALUES (:#{body.customer}, :#{body.country}, 1)"
+ """;
+ List<String> errors = SourceValidator.validateYamlEndpoints(yaml,
catalog);
+ assertThat(errors).anyMatch(e -> e.startsWith("Line 6: sql:
:#{body.customer} is not a camel-sql named parameter")
+ && e.contains(
+ "write :#${body[customer]} (a Simple expression) or
:#customer for a header or a key of a Map body")
+ && e.endsWith("(also :#{body.country})"));
+
+ // under parameters, and an expression that is not a field of the body
+ yaml = """
+ - route:
+ from:
+ uri: file:orders
+ steps:
+ - to:
+ uri: sql
+ parameters:
+ query: "SELECT * FROM customers WHERE id =
:#{header.id}"
+ """;
+ errors = SourceValidator.validateYamlEndpoints(yaml, catalog);
+ assertThat(errors).anyMatch(e -> e.startsWith("Line 8: sql:
:#{header.id} is not a camel-sql named parameter")
+ && e.contains("write :#${header.id} (a Simple expression)"));
+ }
+
@Test
void theCamelFormsCastsTimesAndOtherComponentsAreFine() {
String yaml