This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-25208-code-assist in repository https://gitbox.apache.org/repos/asf/camel.git
commit 6cf6f6f154c3d56bf34bae0d7d231d380707cb35 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 30 22:51:52 2026 +0200 CAMEL-25208: camel-jbang - camel validate and camel_validate_source check Java and XML routes SourceValidator.validate, shared by the TUI, the MCP tools (camel_validate_source, camel_write_file) and camel validate source, adds the Camel checks of RouteAssist to Java files (after javac) and to well formed XML DSL files: what the compiler cannot see inside the strings. camel_validate_source also lists the parts of a Java route the parser could not read (notChecked), so an agent knows what was not checked. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/ai/AuthoringTools.java | 15 ++- .../jbang/core/commands/ai/SourceValidator.java | 36 ++++++- .../commands/ai/SourceValidatorRoutesTest.java | 113 +++++++++++++++++++++ .../jbang/core/commands/mcp/AuthoringTools.java | 2 +- 4 files changed, 162 insertions(+), 4 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java index eb0fce3fbe07..064ffd66967e 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java @@ -146,7 +146,7 @@ public final class AuthoringTools { })); registry.accept(tool("camel_validate_source", - "Validates Camel YAML DSL or .properties source without writing: schema (misspelled options such as " + "Validates Camel YAML/Java/XML DSL or .properties source without writing: schema (misspelled options such as " + "logLevel instead of loggingLevel), endpoint URIs, simple expressions, " + "camel.* options. Use on content before writing it, or on an existing " + "file (no content) to explain a reload error.") @@ -497,6 +497,19 @@ public final class AuthoringTools { result.put("valid", errors.isEmpty()); result.put("file", file); result.put("errors", new JsonArray(errors)); + if (RouteAssist.supports(file, content)) { + // the parts of a Java or XML route the parser could not read, so the checks did not see (CAMEL-25208) + JsonArray notChecked = new JsonArray(); + for (RouteAssist.Diagnostic d : RouteAssist.diagnostics(file, content, ctx.catalog(), null, + RouteAssist.javaSources(dir), false)) { + if (d.severity() == RouteAssist.Severity.INFO) { + notChecked.add(d.format()); + } + } + if (!notChecked.isEmpty()) { + result.put("notChecked", notChecked); + } + } result.put("message", errors.isEmpty() ? "The source is valid" : errors.size() + " problem(s) found; fix them before writing the file"); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java index 459be17c7fe7..aac0dfa51683 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java @@ -140,17 +140,49 @@ public final class SourceValidator { if (directory != null && !msgs.isEmpty()) { msgs = withSiblingClassHints(msgs, BeanDeclarations.scan(directory, fileName)); } - return msgs; + // what the compiler cannot see: the endpoint uris and simple expressions inside the strings (CAMEL-25208) + return withRouteChecks(msgs, fileName, content, catalog, directory, checkConsumers); } if (name.endsWith(".xsl") || name.endsWith(".xslt")) { return validateXslt(content); } if (name.endsWith(".xml")) { - return validateXml(content); + List<String> msgs = validateXml(content); + // a well formed file of the XML DSL: its routes against the XML DSL and the catalog (CAMEL-25208) + return msgs.isEmpty() ? withRouteChecks(msgs, fileName, content, catalog, directory, checkConsumers) : msgs; } return List.of(); } + /** + * Adds the errors of the Camel checks of a Java or XML DSL source ({@link RouteAssist}) to the messages, leaving + * out one on a line that already has a message. + */ + private static List<String> withRouteChecks( + List<String> msgs, String fileName, String content, CamelCatalog catalog, Path directory, + boolean checkConsumers) { + if (!RouteAssist.supports(fileName, content)) { + return msgs; + } + List<String> answer = new ArrayList<>(msgs); + Set<String> linesWithMessages = new HashSet<>(); + for (String m : msgs) { + Matcher lm = MESSAGE_LINE.matcher(m); + if (lm.find()) { + linesWithMessages.add(lm.group(1)); + } + } + for (RouteAssist.Diagnostic d : RouteAssist.diagnostics(fileName, content, catalog, directory, + RouteAssist.javaSources(directory), checkConsumers && directory != null)) { + if (d.severity() == RouteAssist.Severity.ERROR && !linesWithMessages.contains(Integer.toString(d.line()))) { + answer.add(d.format()); + } + } + return answer; + } + + private static final Pattern MESSAGE_LINE = Pattern.compile("^Line (\\d+)\\b"); + /** * Validates Camel YAML DSL source: the YAML DSL schema first, then endpoint URIs and simple expressions against the * catalog. The schema is the one of the catalog's Camel version: the CLI's own, or for a catalog of another version diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorRoutesTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorRoutesTest.java new file mode 100644 index 000000000000..f28bcf158b74 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorRoutesTest.java @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.ai; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.util.json.JsonArray; +import org.apache.camel.util.json.JsonObject; +import org.apache.camel.util.json.Jsoner; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The Camel checks of Java and XML DSL routes in the validation shared by the TUI, camel_validate_source and camel + * validate (CAMEL-25208). + */ +class SourceValidatorRoutesTest { + + private static CamelCatalog catalog; + + @BeforeAll + static void loadCatalog() { + catalog = new DefaultCamelCatalog(); + } + + private static final String JAVA = """ + import org.apache.camel.builder.RouteBuilder; + + public class MyRoute extends RouteBuilder { + @Override + public void configure() throws Exception { + from("timer:tick?peroid=1000") + .process(e -> e.getMessage().setBody("hello")) + .to("seda:out"); + } + } + """; + + @Test + void theEndpointsOfAJavaRouteAreChecked() { + List<String> errors = SourceValidator.validate("MyRoute.java", JAVA, catalog, null, null); + assertThat(errors).containsExactly( + "Line 6: timer: Unknown option 'peroid'. Did you mean: [period]"); + } + + @Test + void aJavaCompileErrorKeepsItsLine() { + String src = JAVA.replace(".to(\"seda:out\");", ".to(\"seda:out\")"); + List<String> errors = SourceValidator.validate("MyRoute.java", src, catalog, null, null); + assertThat(errors).isNotEmpty(); + assertThat(errors.get(0)).contains("';' expected"); + // the Camel checks still run on what the parser reads + assertThat(errors).anyMatch(e -> e.contains("Unknown option 'peroid'")); + } + + @Test + void theRoutesOfAnXmlFileAreChecked() { + String xml = """ + <routes xmlns="http://camel.apache.org/schema/xml-io"> + <route> + <from uri="timer:tick"/> + <to uri="seda:${header.queue}"/> + </route> + </routes> + """; + List<String> errors = SourceValidator.validate("routes.xml", xml, catalog, null, null); + assertThat(errors).hasSize(1); + assertThat(errors.get(0)).startsWith("Line 4: ").contains("toD"); + } + + @Test + void anXmlFileThatIsNotWellFormedIsReportedAsSuch() { + String xml = "<routes><route><from uri=\"timer:tick\"/></routes>"; + List<String> errors = SourceValidator.validate("routes.xml", xml, catalog, null, null); + assertThat(errors).hasSize(1); + assertThat(errors.get(0)).contains("XML is not well formed"); + } + + @Test + void theValidateToolSaysWhatWasNotChecked() throws Exception { + String src = JAVA.replace(".to(\"seda:out\");", ".to(\"seda:out?\" + options());") + .replace(" @Override", " String options() { return \"size=1\"; }\n\n @Override"); + Map<String, String> args = new HashMap<>(); + args.put("file", "MyRoute.java"); + args.put("content", src); + JsonObject result = (JsonObject) Jsoner.deserialize( + String.valueOf(ToolRegistry.execute("camel_validate_source", new ToolContext(), args))); + assertThat((JsonArray) result.get("errors")).hasSize(1); + JsonArray notChecked = (JsonArray) result.get("notChecked"); + assertThat(notChecked).hasSize(1); + assertThat(notChecked.getString(0)).startsWith("Line 10: not checked: ").contains("options()"); + } +} diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java index 7f3b43bc2e86..2948c1e2fd00 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/AuthoringTools.java @@ -110,7 +110,7 @@ public class AuthoringTools { } @Tool(annotations = @Tool.Annotations(readOnlyHint = true, destructiveHint = false, openWorldHint = false), - description = "Validates Camel YAML DSL or .properties source without writing: schema (misspelled options " + description = "Validates Camel YAML/Java/XML DSL or .properties source without writing: schema (misspelled options " + "such as logLevel instead of loggingLevel), endpoint URIs, simple expressions, camel.* " + "options, and how each bean under beans: is created (the properties a class built through " + "its builder() accepts, the ways to create a class with no constructor). Use on content "
