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 eadb58569de383efdd204deb39ecdce51143c41f Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 30 23:02:30 2026 +0200 CAMEL-25208: camel-jbang - TUI Source tab completion in the endpoint uris of Java routes Tab in the string of from, to, toD, wireTap, enrich, pollEnrich or poll of a Java route file completes the component name, the endpoint options (by consumer or producer role, the given ones left out) and their values, with the providers the YAML uris have. The string the cursor is in is read from the line alone (JavaStringContext), so it works while the file does not parse. The completion providers fall back to the CLI's catalog when no integration is selected. The completion popup puts an exact or prefix match first: typing seda selected hazelcast-seda before, for YAML as well. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../jbang/core/commands/tui/AutocompletePopup.java | 10 ++ .../jbang/core/commands/tui/JavaStringContext.java | 85 +++++++++++ .../jbang/core/commands/tui/SourceEditAssist.java | 6 +- .../dsl/jbang/core/commands/tui/SourceTab.java | 6 +- .../dsl/jbang/core/commands/tui/SourceViewer.java | 103 +++++++++++++ .../src/main/resources/tui/help/source.md | 8 +- .../commands/tui/JavaStringCompletionTest.java | 165 +++++++++++++++++++++ 7 files changed, 377 insertions(+), 6 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java index faa66df23bb4..8de140e54531 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AutocompletePopup.java @@ -17,6 +17,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import dev.tamboui.layout.Rect; @@ -535,10 +536,19 @@ class AutocompletePopup { filteredItems.add(item); } } + // what was typed exactly first, then what starts with it: seda before hazelcast-seda + filteredItems.sort(Comparator.comparingInt(item -> rank(item.key(), f))); } listState.select(filteredItems.isEmpty() ? null : 0); } + private static int rank(String key, String filter) { + if (key.equalsIgnoreCase(filter)) { + return 0; + } + return key.regionMatches(true, 0, filter, 0, filter.length()) ? 1 : 2; + } + private static boolean matchesLabel(String group, String filter) { if (group == null || group.isEmpty()) { return false; diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringContext.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringContext.java new file mode 100644 index 000000000000..68613c4c8c92 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringContext.java @@ -0,0 +1,85 @@ +/* + * 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.util.Set; + +/** + * The string literal of a Java line the cursor is in, and the method it is an argument of: to("kafka:orders?bro| is in + * to, with kafka:orders?bro before the cursor. Read from the line alone, so it works on a line being typed, which does + * not parse (CAMEL-25208). + * + * @param call the method the literal is the first text of the arguments of, such as to or from + * @param before the text of the literal before the cursor + */ +record JavaStringContext(String call, String before) { + + /** The DSL methods whose argument is an endpoint uri. */ + static final Set<String> ENDPOINT_CALLS = Set.of("from", "to", "toD", "wireTap", "enrich", "pollEnrich", "poll"); + /** The ones of them that consume from their endpoint. */ + static final Set<String> CONSUMER_CALLS = Set.of("from", "pollEnrich", "poll"); + + /** The context at the cursor, or null when the cursor is not in a string literal given to a method. */ + static JavaStringContext at(String line, int col) { + if (line == null || col < 0 || col > line.length()) { + return null; + } + int open = -1; + for (int i = 0; i < col; i++) { + char c = line.charAt(i); + if (open >= 0) { + if (c == '\\') { + i++; + } else if (c == '"') { + open = -1; + } + } else if (c == '"') { + open = i; + } else if (c == '/' && i + 1 < line.length() && line.charAt(i + 1) == '/') { + // a line comment: no string literal after it + return null; + } + } + if (open < 0) { + return null; + } + String head = line.substring(0, open).stripTrailing(); + if (!head.endsWith("(")) { + return null; + } + head = head.substring(0, head.length() - 1).stripTrailing(); + int start = head.length(); + while (start > 0 && Character.isJavaIdentifierPart(head.charAt(start - 1))) { + start--; + } + String call = head.substring(start); + if (call.isEmpty()) { + return null; + } + return new JavaStringContext(call, line.substring(open + 1, col)); + } + + /** Whether the literal is the endpoint uri of a DSL method. */ + boolean isEndpoint() { + return ENDPOINT_CALLS.contains(call); + } + + /** consumer or producer, as the completions of the endpoint options are filtered. */ + String role() { + return CONSUMER_CALLS.contains(call) ? "consumer" : "producer"; + } +} 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 e3e093424fca..c53c171f4427 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 @@ -877,7 +877,7 @@ final class SourceEditAssist { if (!context.startsWith("yaml:")) { return List.of(); } - CamelCatalog catalog = getCatalog(); + CamelCatalog catalog = validationCatalog(); if (catalog == null) { return List.of(); } @@ -946,7 +946,7 @@ final class SourceEditAssist { } List<AutocompletePopup.CompletionItem> provideComponentNameCompletions(String role) { - CamelCatalog catalog = getCatalog(); + CamelCatalog catalog = validationCatalog(); if (catalog == null) { return List.of(); } @@ -1231,7 +1231,7 @@ final class SourceEditAssist { if (!context.startsWith("yaml:")) { return List.of(); } - CamelCatalog catalog = getCatalog(); + CamelCatalog catalog = validationCatalog(); if (catalog == null) { return List.of(); } 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 69c3a283a4c1..477cd05b6957 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 @@ -826,6 +826,7 @@ class SourceTab extends AbstractTab { boolean routeFile = !SourceEditAssist.isYamlFile(filePath) && (isJavaRouteFile(filePath) || isCamelSourceFile(filePath)); sourceViewer.setRouteValidator(routeFile ? content -> assist.validateRoutes(filePath, content) : null); + sourceViewer.setJavaStringCompletion(routeFile && filePath.getFileName().toString().endsWith(".java")); if (isCamelSourceFile(filePath)) { sourceViewer.setQuickDocProvider(assist::provideCamelQuickDocs); sourceViewer.setDeprecatedLineScanner(null); @@ -848,8 +849,9 @@ class SourceTab extends AbstractTab { // Java: the quick doc of the routes read into the model by the Java DSL parser (CAMEL-25208) sourceViewer.setQuickDocProvider(cd -> assist.provideRouteQuickDocs(filePath, cd)); sourceViewer.setDeprecatedLineScanner(null); - sourceViewer.setAutocompleteProvider(null); - sourceViewer.setAutocompleteValueProvider(null); + // Tab in the endpoint uri of from, to, toD...: component names, options and values, as YAML uris have + sourceViewer.setAutocompleteProvider(assist::provideYamlKeyCompletions); + sourceViewer.setAutocompleteValueProvider(assist::provideYamlValueCompletions); sourceViewer.setEditQuickDocProvider((lines, row) -> assist.provideRouteEditQuickDoc(filePath, lines, row)); } else if (SourceEditAssist.isPropertiesFile(filePath)) { sourceViewer.setQuickDocProvider(assist::providePropertiesQuickDocs); 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 789027b0722f..e3a1dd6e4650 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 @@ -188,6 +188,9 @@ class SourceViewer { private EndpointValidator endpointValidator; private EndpointValidator simpleValidator; private EndpointValidator routeValidator; + private boolean javaStringCompletion; + /** The completion being chosen in a Java string: its row, the column its prefix ends at, the prefix and suffix. */ + private JavaCompletion javaCompletion; private List<String> routeProblems = List.of(); private List<String> validationErrors; private int validationErrorScroll; @@ -268,6 +271,17 @@ class SourceViewer { this.routeValidator = routeValidator; } + /** + * Tab completion inside the strings of a Java route file (CAMEL-25208): in the endpoint uri of from, to, toD... the + * component names, the endpoint options and their values, from the completion providers the YAML uris use. + */ + void setJavaStringCompletion(boolean javaStringCompletion) { + this.javaStringCompletion = javaStringCompletion; + } + + private record JavaCompletion(int row, int endCol, String prefix, String suffix) { + } + void hide() { exitEditMode(); visible = false; @@ -280,6 +294,7 @@ class SourceViewer { endpointValidator = null; simpleValidator = null; routeValidator = null; + javaStringCompletion = false; } void reset() { @@ -320,6 +335,7 @@ class SourceViewer { endpointValidator = null; simpleValidator = null; routeValidator = null; + javaStringCompletion = false; } boolean isMarkdownMode() { @@ -995,13 +1011,96 @@ class SourceViewer { } private void openAutocomplete() { + javaCompletion = null; if (isCamelYamlFile()) { openYamlAutocomplete(); + } else if (javaStringCompletion) { + openJavaStringAutocomplete(); } else { openPropertiesAutocomplete(); } } + /** + * The completion of the endpoint uri the cursor is in, in a Java route: the component before the colon, an option + * after ? or &, its value after =. Read from the line alone, so it works while the file does not parse. + */ + private void openJavaStringAutocomplete() { + int row = editState.cursorRow(); + int col = editState.cursorCol(); + JavaStringContext c = JavaStringContext.at(editState.getLine(row), col); + if (c == null || !c.isEndpoint() || autocompleteProvider == null) { + return; + } + String before = c.before(); + int colon = before.indexOf(':'); + List<AutocompletePopup.CompletionItem> items; + String prefix; + String suffix; + if (colon < 0) { + items = autocompleteProvider.provide("yaml-uri:" + c.role()); + prefix = before; + suffix = ":"; + } else { + String scheme = before.substring(0, colon); + int q = before.indexOf('?'); + if (q < 0) { + // the path of the uri: what goes there is the component's own + return; + } + int sep = Math.max(before.lastIndexOf('?'), before.lastIndexOf('&')); + String segment = before.substring(sep + 1); + int eq = segment.indexOf('='); + if (eq >= 0) { + if (autocompleteValueProvider == null) { + return; + } + items = autocompleteValueProvider.provide("yaml:" + scheme + ":" + segment.substring(0, eq)); + prefix = segment.substring(eq + 1); + suffix = ""; + } else { + List<String> given = new ArrayList<>(); + for (String pair : (sep > q ? before.substring(q + 1, sep) : "").split("&")) { + if (!pair.isEmpty()) { + given.add(pair.contains("=") ? pair.substring(0, pair.indexOf('=')) : pair); + } + } + items = autocompleteProvider.provide("yaml:" + scheme + ":" + c.role() + ":" + String.join(",", given) + + "|" + before.substring(0, sep)); + prefix = segment; + suffix = "="; + } + } + if (items != null && !items.isEmpty()) { + autocompletePopup = new AutocompletePopup(items, prefix, prefix, true); + if (colon < 0) { + autocompletePopup.setTitlePrefix("Components"); + } + javaCompletion = new JavaCompletion(row, col, prefix, suffix); + } + } + + /** Replaces the prefix the completion was opened on with the chosen item and what follows it (: or =). */ + private void insertJavaCompletion(AutocompletePopup.CompletionItem item) { + JavaCompletion jc = javaCompletion; + javaCompletion = null; + if (editState.cursorRow() != jc.row()) { + return; + } + // the arrows move the cursor within the prefix while the popup is open: back to its end + int col = editState.cursorCol(); + for (; col < jc.endCol(); col++) { + editState.moveCursorRight(); + } + for (; col > jc.endCol(); col--) { + editState.moveCursorLeft(); + } + for (int i = 0; i < jc.prefix().length(); i++) { + editState.deleteBackward(); + } + editState.insert(item.key() + jc.suffix()); + } + private void openPropertiesAutocomplete() { String lineText = editState.getLine(editState.cursorRow()); int col = editState.cursorCol(); @@ -1167,6 +1266,10 @@ class SourceViewer { private void insertCompletion(AutocompletePopup.CompletionItem item, boolean valueMode, boolean listItem) { recordEditChange(); + if (javaCompletion != null) { + insertJavaCompletion(item); + return; + } String currentLine = editState.getLine(editState.cursorRow()); if (isCamelYamlFile()) { insertYamlCompletion(item, valueMode, currentLine, listItem); 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 b6eb604a176f..2539d637f9ad 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 @@ -72,8 +72,14 @@ Press **F4** to enter edit mode, then **Tab** for context-aware completion: - Value completion shows enum choices, boolean values, and `{{placeholder}}` suggestions from your `.properties` files +**Java DSL routes:** +- In the endpoint uri of `from`, `to`, `toD`, `wireTap`, `enrich`, `pollEnrich` + and `poll`, Tab completes the component name before the `:`, the endpoint + options after `?` or `&` (filtered by consumer/producer role, already given + ones left out), and the value of an option after `=` + Use **Up/Down** to navigate, **Enter** to accept, **Esc** to dismiss, and -type to filter the completion list. +type to filter the completion list (an exact or prefix match comes first). ## Route Jump Links Lines with `to:`, `toD:`, `wireTap:`, or similar endpoints that reference diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringCompletionTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringCompletionTest.java new file mode 100644 index 000000000000..ad7a28b4e0a2 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/JavaStringCompletionTest.java @@ -0,0 +1,165 @@ +/* + * 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; + +/** + * Tab completion in the endpoint uris of a Java route file (CAMEL-25208). + */ +class JavaStringCompletionTest { + + @TempDir + Path tempDir; + + @BeforeEach + void setUp() { + Theme.resetForTesting(); + } + + @Test + void theStringTheCursorIsIn() { + JavaStringContext c = JavaStringContext.at(" from(\"timer:tick?per\")", 28); + assertThat(c.call()).isEqualTo("from"); + assertThat(c.before()).isEqualTo("timer:tick?per"); + assertThat(c.isEndpoint()).isTrue(); + assertThat(c.role()).isEqualTo("consumer"); + + c = JavaStringContext.at(" .to( \"kaf", 21); + assertThat(c.call()).isEqualTo("to"); + assertThat(c.before()).isEqualTo("kaf"); + assertThat(c.role()).isEqualTo("producer"); + + // after the string, in another call, in a comment, not in a string + assertThat(JavaStringContext.at(" from(\"timer:tick\").to(", 30)).isNull(); + // the second string of a concatenation is not the argument's start + assertThat(JavaStringContext.at(" log.info(\"x\" + \"y", 25)).isNull(); + assertThat(JavaStringContext.at(" log.info(\"x", 19).call()).isEqualTo("info"); + assertThat(JavaStringContext.at(" // to(\"kaf", 18)).isNull(); + assertThat(JavaStringContext.at(" x = \"kaf", 17)).isNull(); + // an escaped quote stays in the string + assertThat(JavaStringContext.at(" to(\"a\\\"b", 16).before()).isEqualTo("a\\\"b"); + } + + private SourceViewer viewer(String src) throws Exception { + Path file = tempDir.resolve("MyRoute.java"); + Files.writeString(file, src, StandardCharsets.UTF_8); + AtomicReference<List<IntegrationInfo>> data = new AtomicReference<>(List.of()); + AtomicReference<List<InfraInfo>> infraData = new AtomicReference<>(List.of()); + SourceEditAssist assist = new SourceEditAssist(new MonitorContext(data, infraData)); + SourceViewer viewer = new SourceViewer(); + viewer.setAutocompleteProvider(assist::provideYamlKeyCompletions); + viewer.setAutocompleteValueProvider(assist::provideYamlValueCompletions); + viewer.setJavaStringCompletion(true); + viewer.loadFile(file); + viewer.enterEditMode(); + return viewer; + } + + /** Puts the cursor on the line, before the given text at its end. */ + private static void cursorAt(SourceViewer viewer, int row, String tail) { + for (int i = 0; i < row; i++) { + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.DOWN, KeyModifiers.NONE)); + } + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.END, KeyModifiers.NONE)); + for (int i = 0; i < tail.length(); i++) { + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.LEFT, KeyModifiers.NONE)); + } + } + + private static void type(SourceViewer viewer, String text) { + for (char ch : text.toCharArray()) { + viewer.handleKeyEvent(KeyEvent.ofChar(ch, KeyModifiers.NONE)); + } + } + + private static String line(SourceViewer viewer, int row) { + return viewer.editText().split("\n")[row]; + } + + @Test + void theComponentOfAnEndpoint() throws Exception { + SourceViewer viewer = viewer(""" + public class MyRoute extends RouteBuilder { + public void configure() { + from("timer:tick") + .to("sed"); + } + } + """); + cursorAt(viewer, 3, "\");"); + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.TAB, KeyModifiers.NONE)); + // the popup filters as the prefix is typed further, then Enter takes the selected component + type(viewer, "a"); + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE)); + assertThat(line(viewer, 3)).isEqualTo(" .to(\"seda:\");"); + } + + @Test + void anOptionAndItsValue() throws Exception { + SourceViewer viewer = viewer(""" + public class MyRoute extends RouteBuilder { + public void configure() { + from("timer:tick") + .to("file:out?fileExi"); + } + } + """); + cursorAt(viewer, 3, "\");"); + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.TAB, KeyModifiers.NONE)); + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE)); + assertThat(line(viewer, 3)).isEqualTo(" .to(\"file:out?fileExist=\");"); + + // the cursor is after the = now: the values of the option + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.TAB, KeyModifiers.NONE)); + type(viewer, "Appe"); + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE)); + assertThat(line(viewer, 3)).isEqualTo(" .to(\"file:out?fileExist=Append\");"); + } + + @Test + void noCompletionOutsideAnEndpoint() throws Exception { + SourceViewer viewer = viewer(""" + public class MyRoute extends RouteBuilder { + public void configure() { + from("timer:tick") + .log("sed"); + } + } + """); + String before = viewer.editText(); + cursorAt(viewer, 3, "\");"); + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.TAB, KeyModifiers.NONE)); + viewer.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE)); + // no popup: Enter splits the line as in any editor + assertThat(viewer.editText()).isNotEqualTo(before); + assertThat(viewer.editText()).doesNotContain("seda:"); + } +}
