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 0bdf10654f0f074871497000e0e1af534441656b Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 30 22:55:20 2026 +0200 CAMEL-25208: camel-jbang - TUI Source tab checks Java and XML routes while editing The Source tab runs the Camel checks of RouteAssist on Java route files and Camel XML files, as it does the YAML checks: the problems are marked on their lines while typing (F9 jumps to the next) and said when saving. Unlike YAML they never block the save: a Java file is the application's code, and what the checks cannot know must not keep it from being saved. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../jbang/core/commands/tui/SourceEditAssist.java | 37 ++++++ .../dsl/jbang/core/commands/tui/SourceTab.java | 4 + .../dsl/jbang/core/commands/tui/SourceViewer.java | 45 ++++++- .../src/main/resources/tui/help/source.md | 4 + .../commands/tui/SourceRouteValidationTest.java | 141 +++++++++++++++++++++ 5 files changed, 229 insertions(+), 2 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java index ca1c8e260148..238e0c378479 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java @@ -29,13 +29,16 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeSet; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.camel.catalog.CamelCatalog; import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.dsl.jbang.core.commands.ai.RouteAssist; import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator; import org.apache.camel.tooling.model.BaseOptionModel; import org.apache.camel.tooling.model.ComponentModel; @@ -76,6 +79,12 @@ final class SourceEditAssist { private final Map<String, Map<String, BaseOptionModel>> languageOptionsCache = new HashMap<>(); private final Map<String, Map<String, BaseOptionModel>> dataformatOptionsCache = new HashMap<>(); + // Java sources of the project, for the constants of the Java route checks (read lazily, refreshed now and then) + private static final long JAVA_SOURCES_TTL_MS = 10_000; + private Map<String, Supplier<String>> javaSourcesCache; + private long javaSourcesCacheTime; + private Path javaSourcesCacheDir; + // Component name completion cache (keyed by catalog version) private String componentsCatalogVersion; private List<AutocompletePopup.CompletionItem> consumerComponents; @@ -1552,6 +1561,34 @@ final class SourceEditAssist { return SourceValidator.validateYamlEndpoints(content, validationCatalog()); } + /** + * The Camel checks of a Java or XML DSL route file (CAMEL-25208): the endpoint uris and simple expressions the + * compiler cannot see, as "Line N: message". The endpoints no route consumes are left to camel validate: the editor + * runs this while typing, and would read the whole project each time. + */ + List<String> validateRoutes(Path file, String content) { + List<String> answer = new ArrayList<>(); + for (RouteAssist.Diagnostic d : RouteAssist.diagnostics(file.getFileName().toString(), content, + validationCatalog(), null, javaSources(), false)) { + if (d.severity() == RouteAssist.Severity.ERROR) { + answer.add(d.format()); + } + } + return answer; + } + + /** The Java sources of the project, for the constants a route refers to in another class; kept for a while. */ + private Map<String, Supplier<String>> javaSources() { + long now = System.currentTimeMillis(); + if (javaSourcesCache == null || now - javaSourcesCacheTime > JAVA_SOURCES_TTL_MS + || !Objects.equals(javaSourcesCacheDir, rootDir)) { + javaSourcesCache = RouteAssist.javaSources(rootDir); + javaSourcesCacheTime = now; + javaSourcesCacheDir = rootDir; + } + return javaSourcesCache; + } + List<String> validateYamlSimple(String content) { return SourceValidator.validateYamlSimple(content, validationCatalog()); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java index 22fab2b563c3..f138f7262cee 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java @@ -822,6 +822,10 @@ class SourceTab extends AbstractTab { * type (Camel YAML, other Camel source, properties, or none). */ private void configureEditAssist(Path filePath) { + // the Camel checks of a Java or XML DSL route file (CAMEL-25208); YAML has its own + boolean routeFile = !SourceEditAssist.isYamlFile(filePath) + && (isJavaRouteFile(filePath) || isCamelSourceFile(filePath)); + sourceViewer.setRouteValidator(routeFile ? content -> assist.validateRoutes(filePath, content) : null); if (isCamelSourceFile(filePath)) { sourceViewer.setQuickDocProvider(assist::provideCamelQuickDocs); sourceViewer.setDeprecatedLineScanner(null); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java index 1bdb50ecff74..789027b0722f 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java @@ -187,6 +187,8 @@ class SourceViewer { private PropertiesValidator propertiesValidator; private EndpointValidator endpointValidator; private EndpointValidator simpleValidator; + private EndpointValidator routeValidator; + private List<String> routeProblems = List.of(); private List<String> validationErrors; private int validationErrorScroll; private Map<Integer, String> inlineErrors = Collections.emptyMap(); @@ -257,6 +259,15 @@ class SourceViewer { this.simpleValidator = simpleValidator; } + /** + * The Camel checks of a Java or XML DSL route file (CAMEL-25208): its problems are marked on their lines while + * editing and said on save, which they do not block - a Java file is the application's code, and what the checks + * cannot know must never keep it from being saved. + */ + void setRouteValidator(EndpointValidator routeValidator) { + this.routeValidator = routeValidator; + } + void hide() { exitEditMode(); visible = false; @@ -268,6 +279,7 @@ class SourceViewer { propertiesValidator = null; endpointValidator = null; simpleValidator = null; + routeValidator = null; } void reset() { @@ -307,6 +319,7 @@ class SourceViewer { propertiesValidator = null; endpointValidator = null; simpleValidator = null; + routeValidator = null; } boolean isMarkdownMode() { @@ -635,6 +648,11 @@ class SourceViewer { return editState.text(); } + /** Package-private for tests: the problems marked on the lines of the edit buffer, by 0-based line. */ + Map<Integer, String> inlineErrors() { + return inlineErrors; + } + private boolean handleEditKeyEvent(KeyEvent ke) { if (validationErrors != null) { if (ke.isCancel() || ke.isKey(KeyCode.ENTER)) { @@ -1274,7 +1292,7 @@ class SourceViewer { dirty = false; Path path = editableFile; boolean restoreMarkdownMode = markdownModeBeforeEdit; - notifySave("Saved: " + editableFile.getFileName(), false); + notifySaved(editableFile); editMode = false; editState.clear(); markdownModeBeforeEdit = false; @@ -1301,13 +1319,14 @@ class SourceViewer { dirty = false; originalEditText = content; lineStatuses = null; - notifySave("Saved: " + editableFile.getFileName(), false); + notifySaved(editableFile); } catch (IOException e) { notifySave("Save failed: " + e.getMessage(), true); } } private void validateAndNotify(String content) { + routeProblems = List.of(); if (validateOnSave && isCamelYamlFile()) { List<String> msgs = new ArrayList<>(); msgs.addAll(SourceValidationSupport.formatSchemaErrors(validateYaml(content))); @@ -1337,10 +1356,27 @@ class SourceViewer { inlineErrors = buildInlineErrors(msgs, content); return; } + } else if (validateOnSave && routeValidator != null) { + // marked, and said when saved, but not blocking the save + List<String> msgs = routeValidator.validate(content); + routeProblems = msgs != null ? msgs : List.of(); + inlineErrors = routeProblems.isEmpty() ? Collections.emptyMap() : buildInlineErrors(routeProblems, content); + return; } inlineErrors = Collections.emptyMap(); } + /** The notice of a save: the file, and the Camel problems of a Java or XML route file when it has some. */ + private void notifySaved(Path file) { + if (routeProblems.isEmpty()) { + notifySave("Saved: " + file.getFileName(), false); + } else { + notifySave("Saved: " + file.getFileName() + " with " + routeProblems.size() + " Camel problem" + + (routeProblems.size() > 1 ? "s" : "") + ": " + routeProblems.get(0), + true); + } + } + private void jumpToNextError() { List<Integer> errorLines = new ArrayList<>(inlineErrors.keySet()); Collections.sort(errorLines); @@ -1389,6 +1425,11 @@ class SourceViewer { } } else if (isPropertiesFile() && propertiesValidator != null) { msgs.addAll(validateProperties(content)); + } else if (routeValidator != null) { + List<String> routeErrors = routeValidator.validate(content); + if (routeErrors != null) { + msgs.addAll(routeErrors); + } } inlineErrors = msgs.isEmpty() ? Collections.emptyMap() : buildInlineErrors(msgs, content); } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md index b9bdd0b7996a..75e8d5191dc5 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md @@ -38,6 +38,10 @@ explorer on the left and a source viewer on the right. - Quick documentation panel is shown at the bottom (shows doc for current line) - **F7** — show diff of unsaved changes - **F9** — jump to next validation error +- Java and XML DSL routes are checked as you type, like YAML routes: endpoint + options, simple expressions, and a `to` with `${...}` that should be a `toD`. + The problems are marked on their lines and said when saving, but never keep a + Java or XML file from being saved. ## Edit Mode (Tab Completion) Press **F4** to enter edit mode, then **Tab** for context-aware completion: diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRouteValidationTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRouteValidationTest.java new file mode 100644 index 000000000000..99326794c7fb --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRouteValidationTest.java @@ -0,0 +1,141 @@ +/* + * 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.tui; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import dev.tamboui.tui.event.KeyCode; +import dev.tamboui.tui.event.KeyEvent; +import dev.tamboui.tui.event.KeyModifiers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The Camel checks of Java and XML DSL route files in the Source tab (CAMEL-25208): marked on their lines, said on + * save, never blocking it. + */ +class SourceRouteValidationTest { + + 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") + .to("seda:out"); + } + } + """; + + @TempDir + Path tempDir; + + private final AtomicReference<String> lastNotification = new AtomicReference<>(); + private final AtomicReference<Boolean> lastNotificationError = new AtomicReference<>(); + + private static SourceEditAssist assist() { + AtomicReference<List<IntegrationInfo>> data = new AtomicReference<>(List.of()); + AtomicReference<List<InfraInfo>> infraData = new AtomicReference<>(List.of()); + return new SourceEditAssist(new MonitorContext(data, infraData)); + } + + @BeforeEach + void setUp() { + Theme.resetForTesting(); + } + + @Test + void theEndpointsOfAJavaRouteAreChecked() { + List<String> errors = assist().validateRoutes(Path.of("MyRoute.java"), JAVA); + assertThat(errors).containsExactly("Line 6: timer: Unknown option 'peroid'. Did you mean: [period]"); + assertThat(assist().validateRoutes(Path.of("MyRoute.java"), JAVA.replace("peroid", "period"))).isEmpty(); + } + + @Test + void theRoutesOfAnXmlFileAreChecked() { + List<String> errors = assist().validateRoutes(Path.of("routes.xml"), """ + <routes xmlns="http://camel.apache.org/schema/xml-io"> + <route> + <from uri="timer:tick"/> + <filter> + <simple>${header.foo} ==</simple> + <to uri="seda:out"/> + </filter> + </route> + </routes> + """); + assertThat(errors).hasSize(1); + assertThat(errors.get(0)).startsWith("Line 4: Simple syntax error"); + } + + @Test + void aProblemIsMarkedAndSaidButTheFileIsSaved() throws Exception { + Path file = tempDir.resolve("MyRoute.java"); + Files.writeString(file, JAVA.replace("peroid", "period"), StandardCharsets.UTF_8); + SourceViewer viewer = new SourceViewer(); + viewer.setNotificationCallback((msg, error) -> { + lastNotification.set(msg); + lastNotificationError.set(error); + }); + SourceEditAssist assist = assist(); + viewer.setRouteValidator(content -> assist.validateRoutes(file, content)); + viewer.loadFile(file); + viewer.enterEditMode(); + + // line 6: timer:tick?period=1000 becomes timer:tick?peroid=1000 + for (int i = 0; i < 5; i++) { + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, KeyModifiers.NONE)); + } + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.HOME, KeyModifiers.NONE)); + String line = viewer.editText().split("\n")[5]; + int col = line.indexOf("period") + 3; + for (int i = 0; i < col; i++) { + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.RIGHT, KeyModifiers.NONE)); + } + // "per|iod": delete the i, type it after the o + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DELETE, KeyModifiers.NONE)); + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.RIGHT, KeyModifiers.NONE)); + viewer.handleKeyEvent(KeyEvent.ofChar('i', KeyModifiers.NONE)); + assertThat(viewer.editText()).contains("timer:tick?peroid=1000"); + + // save and keep editing: saved, the problem said and marked on its line + viewer.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.CTRL)); + assertThat(Files.readString(file, StandardCharsets.UTF_8)).contains("peroid=1000"); + assertThat(lastNotification.get()).startsWith("Saved: MyRoute.java with 1 Camel problem: Line 6: "); + assertThat(lastNotificationError.get()).isTrue(); + assertThat(viewer.isEditMode()).isTrue(); + assertThat(viewer.inlineErrors()).containsOnlyKeys(5); + + // a clean save says nothing more than that: "peroi|d" back to "period" + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.BACKSPACE, KeyModifiers.NONE)); + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.LEFT, KeyModifiers.NONE)); + viewer.handleKeyEvent(KeyEvent.ofChar('i', KeyModifiers.NONE)); + assertThat(viewer.editText()).contains("timer:tick?period=1000"); + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.F5, KeyModifiers.NONE)); + assertThat(lastNotification.get()).isEqualTo("Saved: MyRoute.java"); + assertThat(lastNotificationError.get()).isFalse(); + assertThat(Files.readString(file, StandardCharsets.UTF_8)).contains("period=1000"); + } +}
