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 255772f0de44 chore: camel-jbang - source editor: a note on a line that
sends to a direct: or seda: endpoint no route consumes (#28342)
255772f0de44 is described below
commit 255772f0de44e5db85f6f7bd04c8228db393de1c
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 23 12:39:07 2026 +0200
chore: camel-jbang - source editor: a note on a line that sends to a
direct: or seda: endpoint no route consumes (#28342)
A YAML route with to: direct:cheese and no from: direct:cheese anywhere in
the
project gets a note on that line (yellow warning, the save is not blocked),
the
check camel validate has had since CAMEL-24955. The endpoints the other
route
files consume (YAML, Java and XML) are read on a background thread and kept
for
10 seconds (ConsumedEndpoints); the file is marked again when a read finds
something else. The setKameletNotes hook of the viewer is now setYamlNotes.
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../ROOT/pages/camel-jbang-tui-source-editor.adoc | 7 +
.../core/commands/ai/EndpointConsumerChecks.java | 43 ++++-
.../ai/SourceValidatorEndpointConsumersTest.java | 46 +++++
.../jbang/core/commands/tui/ConsumedEndpoints.java | 134 ++++++++++++++
.../dsl/jbang/core/commands/tui/SourceTab.java | 11 +-
.../dsl/jbang/core/commands/tui/SourceViewer.java | 20 +-
.../core/commands/tui/SourceConsumerNotesTest.java | 202 +++++++++++++++++++++
.../commands/tui/SourceKameletValidationTest.java | 2 +-
8 files changed, 452 insertions(+), 13 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
index 55a7117b77c2..0de33d4d0efc 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
@@ -90,6 +90,13 @@ panel at the bottom says what is wrong with the line the
cursor is on. A note, s
counted apart in the title as `notes`. A note does not keep the file from
being saved. They are checked again as you type, and *F9*
jumps to the next problem.
+A YAML route that sends to a `direct` or `seda` endpoint no route consumes
gets a note on that line, such as
+`to: direct:cheese` while no route has `from: direct:cheese`. The routes of
the other YAML, Java and XML files of the
+project count too. They are read in the background when the file opens, and
read again every few seconds. It is a
+note rather than an error, as the route that consumes it is often the next one
you write. The check says nothing when
+the other files cannot tell, such as a route whose input is a property
placeholder or a route template, or a project
+with more files than the check reads.
+
image::jbang/camel-tui-source-problems.png[A misspelled seda option marked
while typing]
=== Saving
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointConsumerChecks.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointConsumerChecks.java
index b5688d7b4325..1d39f6fb9ae2 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointConsumerChecks.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointConsumerChecks.java
@@ -30,6 +30,7 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.camel.catalog.CamelCatalog;
@@ -48,6 +49,8 @@ public final class EndpointConsumerChecks {
private static final Pattern JAVA_FROM =
Pattern.compile("(?<![.\\w])from\\s*\\(");
/** The build files of a project whose routes are spread over
src/main/java and src/main/resources. */
private static final List<String> BUILD_FILES = List.of("pom.xml",
"build.gradle", "build.gradle.kts");
+ /** The endpoint of a message of {@link EndpointConsumers#check(String,
Set)}. */
+ private static final Pattern UNCONSUMED = Pattern.compile("sends to
([^,\\s]+), and no route consumes it");
private EndpointConsumerChecks() {
}
@@ -70,13 +73,51 @@ public final class EndpointConsumerChecks {
return EndpointConsumers.check(content, consumed(directory,
excludeFile, catalog));
}
+ /**
+ * As {@link #validateYamlConsumers(String, Path, String, CamelCatalog)},
with the endpoints the other route files
+ * consume already known, as an editor keeps them while the file is typed:
each message is on the lines that send to
+ * the endpoint, as "Line N: message", and without a line prefix when none
of the lines names it as written.
+ *
+ * @param consumedElsewhere what {@link #consumed(Path, String,
CamelCatalog)} returned; null says nothing
+ * @return the messages, one per line that sends to an
endpoint no route consumes
+ */
+ public static List<String> validateYamlConsumers(String content,
Set<String> consumedElsewhere) {
+ List<String> answer = new ArrayList<>();
+ String[] lines = content.split("\n", -1);
+ for (String msg : EndpointConsumers.check(content, consumedElsewhere))
{
+ Matcher m = UNCONSUMED.matcher(msg);
+ List<Integer> found = m.find() ? linesNaming(lines, m.group(1)) :
List.of();
+ if (found.isEmpty()) {
+ answer.add(msg);
+ }
+ for (int line : found) {
+ answer.add("Line " + (line + 1) + ": " + msg);
+ }
+ }
+ return answer;
+ }
+
+ /** The lines, from 0, that name the endpoint (direct:cheese, also as
direct://cheese), comments left out. */
+ private static List<Integer> linesNaming(String[] lines, String endpoint) {
+ int colon = endpoint.indexOf(':');
+ Pattern p = Pattern.compile("(?<![\\w.:/-])" +
Pattern.quote(endpoint.substring(0, colon + 1)) + "(?://)?"
+ + Pattern.quote(endpoint.substring(colon +
1)) + "(?![\\w.-])");
+ List<Integer> answer = new ArrayList<>();
+ for (int i = 0; i < lines.length; i++) {
+ if (!lines[i].trim().startsWith("#") &&
p.matcher(lines[i]).find()) {
+ answer.add(i);
+ }
+ }
+ return answer;
+ }
+
/**
* The {@code direct:} and {@code seda:} endpoints the route files under
the directory consume, leaving out the file
* being validated; null when they cannot be known: no directory, a
directory that is only a part of a Maven or
* Gradle project, more files than the scan looks at, or a route input the
scan cannot read (a Java {@code from(}
* with no literal, a placeholder, a route template), which could be any
endpoint.
*/
- static Set<String> consumed(Path directory, String excludeFile,
CamelCatalog catalog) {
+ public static Set<String> consumed(Path directory, String excludeFile,
CamelCatalog catalog) {
if (directory == null || !Files.isDirectory(directory)) {
return null;
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointConsumersTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointConsumersTest.java
index 287cfb9dbb9e..640e42bb9790 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointConsumersTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointConsumersTest.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
+import java.util.Set;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
@@ -268,4 +269,49 @@ class SourceValidatorEndpointConsumersTest {
void withoutADirectoryTheCheckSaysNothing() {
assertThat(SourceValidator.validate("r.camel.yaml", ROUTE, CATALOG,
null)).isEmpty();
}
+
+ @Test
+ void theEditorGetsTheMessageOnEachLineThatSendsToTheEndpoint() throws
Exception {
+ Files.writeString(dir.resolve("other.camel.yaml"), """
+ - route:
+ from:
+ uri: direct:other
+ steps:
+ - log: other
+ """);
+ Set<String> consumed = EndpointConsumerChecks.consumed(dir,
"r.camel.yaml", CATALOG);
+ assertThat(consumed).containsOnly("direct:other");
+ String route = """
+ - route:
+ id: tick
+ from:
+ uri: timer:tick
+ steps:
+ # to: direct:cheese, not yet
+ - to: direct:cheese
+ - to: direct:cheeseburger
+ - to: direct://cheese?timeout=1000
+ - to: direct:other
+ """;
+ List<String> msgs =
EndpointConsumerChecks.validateYamlConsumers(route, consumed);
+ assertThat(msgs).hasSize(3);
+ assertThat(msgs.get(0)).startsWith("Line 7: route tick: sends to
direct:cheese, and no route consumes it");
+ assertThat(msgs.get(1)).startsWith("Line 9: route tick: sends to
direct:cheese, and no route consumes it");
+ assertThat(msgs.get(2)).startsWith("Line 8: route tick: sends to
direct:cheeseburger");
+ // consumed in the file itself
+ assertThat(EndpointConsumerChecks.validateYamlConsumers(route + """
+ - route:
+ from:
+ uri: direct:cheese
+ steps:
+ - log: cheese
+ - route:
+ from:
+ uri: direct:cheeseburger
+ steps:
+ - log: burger
+ """, consumed)).isEmpty();
+ // the other files not known: nothing is said
+ assertThat(EndpointConsumerChecks.validateYamlConsumers(route,
null)).isEmpty();
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumedEndpoints.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumedEndpoints.java
new file mode 100644
index 000000000000..7cbb40507c22
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ConsumedEndpoints.java
@@ -0,0 +1,134 @@
+/*
+ * 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.file.Path;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.function.Consumer;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.EndpointConsumerChecks;
+
+/**
+ * The direct: and seda: endpoints the other route files of the project
consume, for the editor to mark a line that
+ * sends to one no route consumes (CAMEL-24955). Reading them means reading
every route file of the project, so they are
+ * read on a background thread and kept: the editor asks while typing, gets
what was read last (null until the first
+ * read is done), and is told when a read finds something else. They are read
again when older than a few seconds, as
+ * the other files may be edited meanwhile, in the TUI or outside it.
+ */
+final class ConsumedEndpoints {
+
+ static final long TTL_MS = 10_000;
+
+ private final Executor executor;
+ private final Consumer<Runnable> renderThread;
+
+ // the file asked about last: the endpoints are those of the route files
under directory, excludeFile left out
+ private Path directory;
+ private String excludeFile;
+ private CamelCatalog catalog;
+ private Runnable onChange;
+ private Set<String> consumed;
+ private long readTime;
+ private boolean reading;
+
+ /**
+ * @param executor where the files are read
+ * @param renderThread runs the callback of a read that found something
else on the UI thread
+ */
+ ConsumedEndpoints(Executor executor, Consumer<Runnable> renderThread) {
+ this.executor = executor;
+ this.renderThread = renderThread;
+ }
+
+ /**
+ * The notes of a YAML route file: each line that sends to a direct: or
seda: endpoint no route of the project
+ * consumes, as "Line N: message"; none while the other route files are
being read the first time.
+ *
+ * @param rootDir the project directory, whose route files are read; the
file's own directory when it is not in it
+ * @param onChange called on the UI thread when a read finds other
endpoints, to mark the file again
+ */
+ List<String> notes(Path file, Path rootDir, String content, CamelCatalog
catalog, Runnable onChange) {
+ Path f = file.toAbsolutePath().normalize();
+ Path root = rootDir != null ? rootDir.toAbsolutePath().normalize() :
null;
+ if (root == null || !f.startsWith(root)) {
+ root = f.getParent();
+ }
+ return EndpointConsumerChecks.validateYamlConsumers(content,
+ get(root, root.relativize(f).toString(), catalog, onChange));
+ }
+
+ /**
+ * The endpoints the route files under the directory consume, the excluded
file left out: what was read last for
+ * them, starting a read when there is none yet or it is old.
+ *
+ * @param onChange called on the UI thread when a read finds other
endpoints than were returned
+ * @return null while not known: not read yet, or the files
cannot say (see
+ * {@link EndpointConsumerChecks#consumed})
+ */
+ synchronized Set<String> get(Path directory, String excludeFile,
CamelCatalog catalog, Runnable onChange) {
+ this.catalog = catalog;
+ this.onChange = onChange;
+ if (!Objects.equals(directory, this.directory) ||
!Objects.equals(excludeFile, this.excludeFile)) {
+ this.directory = directory;
+ this.excludeFile = excludeFile;
+ consumed = null;
+ readTime = 0;
+ }
+ if (!reading && (readTime == 0 || MonotonicClock.millis() - readTime >
TTL_MS)) {
+ startRead();
+ }
+ return consumed;
+ }
+
+ /** Reads the endpoints of the file asked about last; the caller holds the
lock. */
+ private void startRead() {
+ reading = true;
+ Path dir = directory;
+ String exclude = excludeFile;
+ CamelCatalog cat = catalog;
+ executor.execute(() -> read(dir, exclude, cat));
+ }
+
+ private void read(Path dir, String exclude, CamelCatalog cat) {
+ Set<String> found = null;
+ try {
+ found = EndpointConsumerChecks.consumed(dir, exclude, cat);
+ } catch (RuntimeException e) {
+ // what the files consume is not known: the check says nothing
+ }
+ Runnable callback;
+ synchronized (this) {
+ if (!Objects.equals(dir, directory) || !Objects.equals(exclude,
excludeFile)) {
+ // another file was opened meanwhile: read for that one instead
+ startRead();
+ return;
+ }
+ reading = false;
+ boolean changed = readTime == 0 ? found != null :
!Objects.equals(found, consumed);
+ consumed = found;
+ readTime = MonotonicClock.millis();
+ callback = changed ? onChange : null;
+ }
+ if (callback != null) {
+ renderThread.accept(callback);
+ }
+ }
+}
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 8eb377e8f88f..0d4fa880c135 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
@@ -85,6 +85,11 @@ class SourceTab extends AbstractTab {
private Rect rightArea;
private final SourceEditAssist assist = new SourceEditAssist(ctx);
+ private final ConsumedEndpoints consumedEndpoints = new
ConsumedEndpoints(ctx.backgroundExecutor, r -> {
+ if (ctx.runner != null) {
+ ctx.runner.runOnRenderThread(r);
+ }
+ });
SourceEditAssist editAssist() {
return assist;
@@ -958,7 +963,11 @@ class SourceTab extends AbstractTab {
name.toLowerCase(Locale.ROOT), content,
assist.validateYamlSimple(content)));
// the shape of a Kamelet file and the kamelet: endpoints of a
route (CAMEL-25411)
sourceViewer.setKameletValidator(content ->
SourceEditAssist.validateKamelets(filePath, content));
- sourceViewer.setKameletNotes(content ->
SourceEditAssist.kameletNotes(filePath, content));
+ // the notes: a Kamelet's unused dependency, a direct: or
seda: endpoint no route consumes (CAMEL-24955)
+ sourceViewer.setYamlNotes(content ->
SourceEditAssist.isKameletFile(filePath)
+ ? SourceEditAssist.kameletNotes(filePath, content)
+ : consumedEndpoints.notes(filePath, rootDir, content,
assist.validationCatalog(),
+ () -> sourceViewer.reloadIfShowing(filePath)));
completion.setListItemNodeChecker(assist::isListChildrenNode);
sourceViewer.setEditQuickDocProvider(withProjectDocs(assist::provideEditQuickDoc));
} else {
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 284e56c1100c..42dd66f70836 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
@@ -258,9 +258,9 @@ class SourceViewer {
private PropertiesValidator propertiesValidator;
private EndpointValidator endpointValidator;
private EndpointValidator simpleValidator;
- // the Kamelet checks of a YAML file (CAMEL-25411): its problems, and the
notes that do not block a save
+ // the Kamelet checks of a YAML file (CAMEL-25411), and the notes of a
YAML file that do not block a save
private EndpointValidator kameletValidator;
- private EndpointValidator kameletNotes;
+ private EndpointValidator yamlNotes;
// the lines whose problem is a note, not an error: marked as a warning
and counted apart (CAMEL-25411)
private Set<Integer> noteLines = Set.of();
private EndpointValidator routeValidator;
@@ -349,11 +349,11 @@ class SourceViewer {
}
/**
- * What a Kamelet file does that works but is not right (a camel:
dependency its template does not use): marked and
- * said when saved, but not blocking the save.
+ * What a YAML file does that works but is not right (a Kamelet's camel:
dependency its template does not use, a
+ * direct: endpoint no route consumes): marked and said when saved, but
not blocking the save.
*/
- void setKameletNotes(EndpointValidator kameletNotes) {
- this.kameletNotes = kameletNotes;
+ void setYamlNotes(EndpointValidator yamlNotes) {
+ this.yamlNotes = yamlNotes;
}
/**
@@ -388,7 +388,7 @@ class SourceViewer {
endpointValidator = null;
simpleValidator = null;
kameletValidator = null;
- kameletNotes = null;
+ yamlNotes = null;
noteLines = Set.of();
routeValidator = null;
cursorQuickDocProvider = null;
@@ -429,7 +429,7 @@ class SourceViewer {
endpointValidator = null;
simpleValidator = null;
kameletValidator = null;
- kameletNotes = null;
+ yamlNotes = null;
noteLines = Set.of();
routeValidator = null;
cursorQuickDocProvider = null;
@@ -1274,7 +1274,7 @@ class SourceViewer {
return;
}
List<String> notes = new ArrayList<>();
- addAll(notes, kameletNotes, content);
+ addAll(notes, yamlNotes, content);
if (!notes.isEmpty()) {
// marked, and said when saved, but not blocking the save
routeProblems = notes;
@@ -1517,7 +1517,7 @@ class SourceViewer {
}
addAll(msgs, kameletValidator, content);
List<String> notes = new ArrayList<>();
- addAll(notes, kameletNotes, content);
+ addAll(notes, yamlNotes, content);
if (!notes.isEmpty()) {
// a line with an error is marked as an error
Set<Integer> lines = new HashSet<>(buildInlineErrors(notes,
content).keySet());
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceConsumerNotesTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceConsumerNotesTest.java
new file mode 100644
index 000000000000..ff9a16cda9cd
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceConsumerNotesTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.ArrayDeque;
+import java.util.Deque;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+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.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+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;
+
+/**
+ * A line of a YAML route that sends to a direct: or seda: endpoint no route
of the project consumes is marked as a note
+ * in the Source tab (CAMEL-24955): a warning that does not block the save, as
the consuming route may not be written
+ * yet. The other route files are read on a background thread, run here by
hand.
+ */
+class SourceConsumerNotesTest {
+
+ private static final CamelCatalog CATALOG = new DefaultCamelCatalog();
+
+ private static final String ROUTE = """
+ - route:
+ id: tick
+ from:
+ uri: timer:tick
+ steps:
+ - to: direct:cheese
+ - to: direct:other
+ """;
+
+ private static final String OTHER = """
+ - route:
+ from:
+ uri: direct:other
+ steps:
+ - log: other
+ """;
+
+ @TempDir
+ Path tempDir;
+
+ private final Deque<Runnable> background = new ArrayDeque<>();
+ private final AtomicReference<String> lastNotification = new
AtomicReference<>();
+
+ @BeforeEach
+ void setUp() {
+ Theme.resetForTesting();
+ }
+
+ @Test
+ void aDirectEndpointNoRouteConsumesIsANoteOnItsLine() throws Exception {
+ Files.writeString(tempDir.resolve("other.camel.yaml"), OTHER,
StandardCharsets.UTF_8);
+ Path file = tempDir.resolve("r.camel.yaml");
+ Files.writeString(file, ROUTE, StandardCharsets.UTF_8);
+ SourceViewer viewer = viewer(file);
+ viewer.loadFile(file);
+ // the other route files are being read: nothing is said yet
+ assertThat(viewer.viewErrors()).isEmpty();
+ runBackground();
+ int cheese = lineOf(ROUTE, "direct:cheese");
+ assertThat(viewer.viewErrors()).containsOnlyKeys(cheese);
+ assertThat(viewer.viewErrors().get(cheese))
+ .startsWith("route tick: sends to direct:cheese, and no route
consumes it");
+ assertThat(viewer.noteLines()).containsOnly(cheese);
+
+ // a note: the file is saved
+ viewer.enterEditMode();
+ assertThat(viewer.inlineErrors()).containsOnlyKeys(cheese);
+ appendSpaceToLine(viewer, 1);
+ viewer.handleKeyEvent(KeyEvent.ofChar('s', KeyModifiers.CTRL));
+ assertThat(Files.readString(file,
StandardCharsets.UTF_8)).contains("id: tick ");
+ assertThat(lastNotification.get()).startsWith("Saved: r.camel.yaml
with 1 Camel problem: ")
+ .contains("sends to direct:cheese");
+ }
+
+ @Test
+ void theNoteGoesWhenARouteConsumesTheEndpoint() throws Exception {
+ Files.writeString(tempDir.resolve("other.camel.yaml"), OTHER,
StandardCharsets.UTF_8);
+ Path file = tempDir.resolve("r.camel.yaml");
+ String withCheese = ROUTE + """
+ - route:
+ from:
+ uri: direct:cheese
+ steps:
+ - log: cheese
+ """;
+ Files.writeString(file, withCheese, StandardCharsets.UTF_8);
+ SourceViewer viewer = viewer(file);
+ viewer.loadFile(file);
+ runBackground();
+ assertThat(viewer.viewErrors()).isEmpty();
+ assertThat(viewer.noteLines()).isEmpty();
+ }
+
+ @Test
+ void theOtherFilesAreReadOnceAndAgainForAnotherFile() throws Exception {
+ Files.writeString(tempDir.resolve("other.camel.yaml"), OTHER,
StandardCharsets.UTF_8);
+ ConsumedEndpoints consumed = new ConsumedEndpoints(background::add,
Runnable::run);
+ AtomicInteger changes = new AtomicInteger();
+ Path file = tempDir.resolve("r.camel.yaml");
+ assertThat(consumed.notes(file, tempDir, ROUTE, CATALOG,
changes::incrementAndGet)).isEmpty();
+ assertThat(background).hasSize(1);
+ // asked again while reading: no second read
+ consumed.notes(file, tempDir, ROUTE, CATALOG,
changes::incrementAndGet);
+ assertThat(background).hasSize(1);
+ runBackground();
+ assertThat(changes).hasValue(1);
+ assertThat(consumed.notes(file, tempDir, ROUTE, CATALOG,
changes::incrementAndGet))
+ .singleElement().asString().startsWith("Line 6: route tick:
sends to direct:cheese");
+ // read a moment ago: kept
+ assertThat(background).isEmpty();
+
+ // the other file opened before the read of the first one is done:
that read is followed by one for it
+ Path other = tempDir.resolve("other.camel.yaml");
+ consumed.notes(file, tempDir, ROUTE, CATALOG,
changes::incrementAndGet);
+ Files.writeString(tempDir.resolve("cheese.camel.yaml"),
OTHER.replace("direct:other", "direct:cheese"),
+ StandardCharsets.UTF_8);
+ consumed.get(tempDir, "nowhere.camel.yaml", CATALOG,
changes::incrementAndGet);
+ assertThat(consumed.notes(other, tempDir, OTHER, CATALOG,
changes::incrementAndGet)).isEmpty();
+ runBackground();
+ assertThat(consumed.get(tempDir, "other.camel.yaml", CATALOG,
changes::incrementAndGet))
+ .containsOnly("direct:cheese");
+ }
+
+ @Test
+ void aFileOutsideTheProjectIsCheckedAgainstItsOwnDirectory() throws
Exception {
+ Path project = Files.createDirectory(tempDir.resolve("project"));
+ Path elsewhere = Files.createDirectory(tempDir.resolve("elsewhere"));
+ Files.writeString(project.resolve("cheese.camel.yaml"),
OTHER.replace("direct:other", "direct:cheese"),
+ StandardCharsets.UTF_8);
+ Files.writeString(elsewhere.resolve("other.camel.yaml"), OTHER,
StandardCharsets.UTF_8);
+ ConsumedEndpoints consumed = new ConsumedEndpoints(background::add,
Runnable::run);
+ Path file = elsewhere.resolve("r.camel.yaml");
+ consumed.notes(file, project, ROUTE, CATALOG, () -> {
+ });
+ runBackground();
+ assertThat(consumed.notes(file, project, ROUTE, CATALOG, () -> {
+ })).singleElement().asString().contains("sends to direct:cheese");
+ }
+
+ private SourceViewer viewer(Path file) {
+ SourceViewer viewer = new SourceViewer();
+ ConsumedEndpoints consumed = new ConsumedEndpoints(background::add,
Runnable::run);
+ viewer.setNotificationCallback((msg, error) ->
lastNotification.set(msg));
+ viewer.setYamlNotes(content -> consumed.notes(file, tempDir, content,
CATALOG,
+ () -> viewer.reloadIfShowing(file)));
+ return viewer;
+ }
+
+ private void runBackground() {
+ while (!background.isEmpty()) {
+ background.poll().run();
+ }
+ }
+
+ /** The line, from 0, that has the text. */
+ private static int lineOf(String content, String text) {
+ List<String> lines = List.of(content.split("\n"));
+ for (int i = 0; i < lines.size(); i++) {
+ if (lines.get(i).contains(text)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /** Moves to the line (from 0) and types a space at its end, a change that
keeps the YAML as it was. */
+ private static void appendSpaceToLine(SourceViewer viewer, int line) {
+ for (int i = 0; i < line; i++) {
+ viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN,
KeyModifiers.NONE));
+ }
+ viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.END, KeyModifiers.NONE));
+ viewer.handleKeyEvent(KeyEvent.ofChar(' ', KeyModifiers.NONE));
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
index 99fa7e49dc5e..d176687e28ed 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceKameletValidationTest.java
@@ -265,7 +265,7 @@ class SourceKameletValidationTest {
SourceViewer viewer = new SourceViewer();
viewer.setNotificationCallback((msg, error) ->
lastNotification.set(msg));
viewer.setKameletValidator(content ->
SourceEditAssist.validateKamelets(file, content));
- viewer.setKameletNotes(content -> SourceEditAssist.kameletNotes(file,
content));
+ viewer.setYamlNotes(content -> SourceEditAssist.kameletNotes(file,
content));
return viewer;
}