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 5750f3739402 CAMEL-23688: camel-jbang - Tests for overview and
transform route (#27967)
5750f3739402 is described below
commit 5750f3739402067121393df5d1c317b2c278643b
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 22:01:12 2026 +0200
CAMEL-23688: camel-jbang - Tests for overview and transform route (#27967)
Tests for camel overview (21% covered), including --ai with a local
server that plays Ollama and --apply-descriptions, and for camel
transform route (29%): Java read without compiling, the conversion to
Java (printed and into a directory) and to an XML file. They found these
bugs, fixed here:
* camel overview printed the markdown for a --format it does not have;
now a usage error.
* camel transform route ran the routes and printed nothing for a
--format it does not have; now a usage error. A route that cannot be
read, such as invalid YAML, failed with an internal error (70) and a
stack trace; now 1 with the error of the parser.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 7 +
.../camel/dsl/jbang/core/commands/Overview.java | 3 +
.../dsl/jbang/core/commands/TransformRoute.java | 17 +-
.../dsl/jbang/core/commands/OverviewTest.java | 254 +++++++++++++++++++++
.../jbang/core/commands/TransformRouteTest.java | 159 +++++++++++++
5 files changed, 439 insertions(+), 1 deletion(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 3920c7303981..6f020a7441ff 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -500,6 +500,13 @@ or follows, like Enter does.
* A plugin that cannot be loaded ends the command with exit code `1` and the
error, as before, but no longer exits the
JVM while the plugins load, which ended a program that embeds the CLI.
+=== camel-jbang - camel overview and camel transform route
+
+* `camel overview --format` other than `markdown` or `json` exits with `2` (it
printed the markdown).
+* `camel transform route --format` other than `xml`, `yaml` or `java` exits
with `2` (it ran the routes and printed
+ nothing), and a route that cannot be read, such as invalid YAML, exits with
`1` and the error of the parser (it failed
+ with an internal error, `70`, and a stack trace).
+
=== camel-jbang - a boolean option given without a value is on
A boolean option that is on by default is now on when it is given without a
value, like any other boolean option:
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Overview.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Overview.java
index b26dda62f52a..79d9a38df945 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Overview.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Overview.java
@@ -129,6 +129,9 @@ public class Overview extends CamelCommand {
@Override
public Integer doCall() throws Exception {
+ if (!"markdown".equals(format) && !"json".equals(format)) {
+ throw CliException.usage("--format must be markdown or json, was:
" + format);
+ }
Path dir = Path.of(directory != null ? directory :
".").toAbsolutePath().normalize();
if (!Files.exists(dir)) {
throw CliException.notFound("Directory does not exist: " + dir);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformRoute.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformRoute.java
index 402ac2f6e31e..1254ecbf9628 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformRoute.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformRoute.java
@@ -19,8 +19,10 @@ package org.apache.camel.dsl.jbang.core.commands;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
+import java.io.FileNotFoundException;
import java.io.InputStream;
import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
@@ -28,6 +30,8 @@ import java.util.List;
import java.util.Stack;
import org.apache.camel.dsl.jbang.core.common.CamelJBangConstants;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
import org.apache.camel.main.KameletMain;
import org.apache.camel.util.IOHelper;
@@ -107,6 +111,9 @@ public class TransformRoute extends CamelCommand {
}
}
+ if (!"xml".equals(format) && !"yaml".equals(format) &&
!"java".equals(format)) {
+ throw CliException.usage("--format must be xml, yaml or java, was:
" + format);
+ }
if ("java".equals(format)) {
return transformToJava();
}
@@ -150,7 +157,15 @@ public class TransformRoute extends CamelCommand {
run.files = files;
run.executionLimitOptions.maxSeconds = 1;
run.mavenResolver = mavenResolver;
- Integer exit = run.runTransform(ignoreLoadingError);
+ Integer exit;
+ try {
+ exit = run.runTransform(ignoreLoadingError);
+ } catch (CliException | NoSuchFileException | FileNotFoundException e)
{
+ throw e;
+ } catch (Exception e) {
+ // a route that cannot be read, such as invalid YAML (the message
says where): not a bug of the CLI
+ throw new CliException(CliExitCode.FAILURE, "Cannot transform the
routes: " + e.getMessage(), e);
+ }
if (exit != null && exit != 0) {
return exit;
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/OverviewTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/OverviewTest.java
new file mode 100644
index 000000000000..823e90ca27bf
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/OverviewTest.java
@@ -0,0 +1,254 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.stream.Stream;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpServer;
+import org.apache.camel.dsl.jbang.core.commands.ai.IntegrationSummary;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+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.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+
+/**
+ * camel overview: the overview of the routes of a project, the summary file,
and what an LLM adds to it (a local server
+ * plays Ollama).
+ */
+class OverviewTest extends CamelCommandBaseTestSupport {
+
+ private static final String ROUTE = """
+ - route:
+ id: hello
+ from:
+ uri: timer:tick
+ steps:
+ - log: Hello Camel
+ """;
+
+ // the answer of the LLM, in the format the summary asks for
+ private static final String ANSWER = """
+ OVERVIEW: The project logs a greeting every second.
+ DESCRIPTIONS:
+ - hello: Say hello | Logs a greeting on a timer.
+ """;
+
+ @TempDir
+ Path project;
+
+ private HttpServer ollama;
+ private volatile String answer = ANSWER;
+
+ @BeforeEach
+ void route() throws Exception {
+ Files.writeString(project.resolve("hello.camel.yaml"), ROUTE);
+ }
+
+ @AfterEach
+ void stopOllama() {
+ if (ollama != null) {
+ ollama.stop(0);
+ }
+ }
+
+ @Test
+ void overviewOfTheRoutes() throws Exception {
+ assertThat(command().doCall()).isZero();
+
+ assertThat(printer.getOutput()).contains("`hello`", "`timer:tick`");
+ }
+
+ @Test
+ void overviewAsJson() throws Exception {
+ assertThat(command("--format=json").doCall()).isZero();
+
+ JsonObject json = Jsoner.deserialize(printer.getOutput(), (JsonObject)
null);
+ JsonArray routes = (JsonArray) json.get("routes");
+ JsonObject route = routes.getMap(0);
+ assertThat(route.getString("route")).isEqualTo("hello");
+ assertThat(route.getString("from")).isEqualTo("timer:tick");
+ }
+
+ @Test
+ void unknownFormatIsAUsageError() {
+ Overview command = command("--format=yaml");
+
+ assertThatThrownBy(command::doCall)
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+ .hasMessage("--format must be markdown or json, was: yaml");
+ }
+
+ @Test
+ void directoryThatDoesNotExistIsNotFound() {
+ Overview command = new Overview(new
CamelJBangMain().withPrinter(printer));
+ CommandLine.populateCommand(command,
project.resolve("nope").toString());
+
+ assertThatThrownBy(command::doCall)
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.NOT_FOUND));
+ }
+
+ @Test
+ void aFileIsNotAProjectDirectory() {
+ Overview command = new Overview(new
CamelJBangMain().withPrinter(printer));
+ CommandLine.populateCommand(command,
project.resolve("hello.camel.yaml").toString());
+
+ assertThatThrownBy(command::doCall)
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE));
+ }
+
+ @Test
+ void aProjectWithoutRoutesIsAFailure() throws Exception {
+ Files.delete(project.resolve("hello.camel.yaml"));
+
+ Overview command = command();
+
+ assertThatThrownBy(command::doCall)
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.FAILURE))
+ .hasMessageStartingWith("No routes found in ");
+ }
+
+ @Test
+ void saveWritesTheSummary() throws Exception {
+ assertThat(command("--save").doCall()).isZero();
+
+ assertThat(project.resolve(IntegrationSummary.FILE_NAME)).exists();
+ assertThat(printer.getStderr()).contains("Saved " +
IntegrationSummary.FILE_NAME);
+ }
+
+ @Test
+ void aiWritesWhatTheLlmAnswersIntoTheSummary() throws Exception {
+ startOllama();
+
+ assertThat(command(ai()).doCall()).isZero();
+
+ String summary =
Files.readString(project.resolve(IntegrationSummary.FILE_NAME));
+ assertThat(summary).contains("The project logs a greeting every
second.", IntegrationSummary.AI_PREFIX + "Say hello");
+ // the summary is printed, and what was saved is on stderr
+ assertThat(printer.getStdout()).contains("Say hello");
+ assertThat(printer.getStderr()).contains("1 route descriptions, 1
notes (AI-assisted");
+ }
+
+ @Test
+ void anAnswerInAnotherFormatIsAFailure() throws Exception {
+ answer = "I am not sure what these routes do.";
+ startOllama();
+
+ Overview command = command(ai());
+
+ assertThatThrownBy(command::doCall)
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.FAILURE))
+ .hasMessageContaining("did not follow the expected format;
nothing was saved");
+ assertThat(printer.getStderr()).contains("I am not sure what these
routes do.");
+
assertThat(project.resolve(IntegrationSummary.FILE_NAME)).doesNotExist();
+ }
+
+ @Test
+ void aiWithoutAnLlmIsAFailure() {
+ // an --url that does not answer is not the end of it: the client goes
on to the Ollama of the host (CAMEL-24772)
+
assumeFalse(LlmClient.create().withApiType(LlmClient.ApiType.ollama).detectEndpoint(),
"the host runs Ollama");
+ Overview command = command("--ai", "--api-type=ollama",
"--url=http://127.0.0.1:1");
+
+ assertThatThrownBy(command::doCall)
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.FAILURE))
+ .hasMessageStartingWith("LLM service is not running or not
reachable");
+ }
+
+ @Test
+ void applyDescriptionsPutsThemIntoTheRoutes() throws Exception {
+ startOllama();
+ assertThat(command(ai()).doCall()).isZero();
+
+ assertThat(command("--apply-descriptions").doCall()).isZero();
+
+
assertThat(Files.readString(project.resolve("hello.camel.yaml"))).contains("Say
hello");
+ assertThat(printer.getStdout()).contains("Updated hello.camel.yaml:
hello");
+ // the description is part of the route now, so the summary no longer
has it as AI-assisted
+
assertThat(Files.readString(project.resolve(IntegrationSummary.FILE_NAME)))
+ .doesNotContain(IntegrationSummary.AI_PREFIX + "Say hello");
+ }
+
+ @Test
+ void applyDescriptionsWithoutASummaryIsAFailure() {
+ Overview command = command("--apply-descriptions");
+
+ assertThatThrownBy(command::doCall)
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.FAILURE))
+ .hasMessageContaining("run camel overview --ai first");
+ }
+
+ private Overview command(String... args) {
+ Overview command = new Overview(new
CamelJBangMain().withPrinter(printer));
+ CommandLine.populateCommand(command,
+ Stream.concat(Stream.of(project.toString()),
Arrays.stream(args)).toArray(String[]::new));
+ return command;
+ }
+
+ private String[] ai() {
+ return new String[] {
+ "--ai", "--api-type=ollama", "--url=http://127.0.0.1:" +
ollama.getAddress().getPort(), "--model=test" };
+ }
+
+ // plays Ollama: its models, and the answer of the model
+ private void startOllama() throws IOException {
+ ollama = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+ ollama.createContext("/api/tags", ex -> reply(ex,
"{\"models\":[{\"name\":\"test\",\"size\":1000}]}"));
+ ollama.createContext("/api/ps", ex -> reply(ex, "{\"models\":[]}"));
+ ollama.createContext("/api/show", ex -> reply(ex,
"{\"model_info\":{}}"));
+ ollama.createContext("/api/generate", ex -> {
+ JsonObject response = new JsonObject();
+ response.put("response", answer);
+ response.put("done", true);
+ reply(ex, response.toJson());
+ });
+ ollama.start();
+ }
+
+ private static void reply(HttpExchange exchange, String body) throws
IOException {
+ exchange.getRequestBody().readAllBytes();
+ byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().add("Content-Type", "application/json");
+ exchange.sendResponseHeaders(200, bytes.length);
+ try (OutputStream out = exchange.getResponseBody()) {
+ out.write(bytes);
+ }
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/TransformRouteTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/TransformRouteTest.java
new file mode 100644
index 000000000000..e9023e187070
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/TransformRouteTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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;
+
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+
+import
org.apache.camel.dsl.jbang.core.commands.exceptionhandler.CliExecutionExceptionHandler;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.catchThrowable;
+
+/**
+ * camel transform route: a route in another DSL, printed or written to a file
or directory.
+ */
+class TransformRouteTest extends CamelCommandBaseTestSupport {
+
+ private static final String YAML = """
+ - route:
+ id: hello
+ from:
+ uri: timer:tick
+ steps:
+ - log: Hello Camel
+ """;
+
+ private static final String JAVA = """
+ import org.apache.camel.builder.RouteBuilder;
+
+ public class Hello extends RouteBuilder {
+ @Override
+ public void configure() throws Exception {
+ from("timer:tick").routeId("hello")
+ .log("Hello Camel");
+ }
+ }
+ """;
+
+ @TempDir
+ Path dir;
+
+ @Test
+ void javaToYamlWithoutCompiling() throws Exception {
+ Path java = Files.writeString(dir.resolve("Hello.java"), JAVA);
+
+ assertThat(command(List.of(java.toString())).doCall()).isZero();
+
+ assertThat(printer.getOutput()).contains("- route:", "id: hello",
"uri: timer", "Hello Camel");
+ }
+
+ @Test
+ void yamlToJava() throws Exception {
+ Path yaml = Files.writeString(dir.resolve("hello.camel.yaml"), YAML);
+
+ assertThat(command(List.of(yaml.toString()),
"--format=java").doCall()).isZero();
+
+ assertThat(printer.getOutput()).contains("extends RouteBuilder",
"from(\"timer:tick\")", "Hello Camel");
+ }
+
+ @Test
+ void yamlToJavaFilesInADirectory() throws Exception {
+ Path one = Files.writeString(dir.resolve("hello.camel.yaml"), YAML);
+ Path two = Files.writeString(dir.resolve("bye.camel.yaml"),
YAML.replace("hello", "bye"));
+ Path out = dir.resolve("java");
+
+ assertThat(command(List.of(one.toString(), two.toString()),
"--format=java", "--output=" + out).doCall()).isZero();
+
+ try (var files = Files.list(out)) {
+ assertThat(files.map(f ->
f.getFileName().toString()).toList()).hasSize(2).allMatch(n ->
n.endsWith(".java"));
+ }
+ assertThat(printer.getOutput()).isEmpty();
+ }
+
+ @Test
+ void javaToXmlFile() throws Exception {
+ Path java = Files.writeString(dir.resolve("Hello.java"), JAVA);
+ Path out = dir.resolve("hello.xml");
+
+ assertThat(command(List.of(java.toString()), "--format=xml",
"--output=" + out).doCall()).isZero();
+
+ assertThat(Files.readString(out)).contains("<route", "id=\"hello\"",
"timer:tick");
+ }
+
+ @Test
+ void unknownFormatIsAUsageError() throws Exception {
+ Path yaml = Files.writeString(dir.resolve("hello.camel.yaml"), YAML);
+
+ TransformRoute command = command(List.of(yaml.toString()),
"--format=json");
+
+ assertThatThrownBy(command::doCall)
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+ .hasMessage("--format must be xml, yaml or java, was: json");
+ }
+
+ @Test
+ void aFileThatDoesNotExistIsNotFound() {
+ TransformRoute command =
command(List.of(dir.resolve("nope.camel.yaml").toString()), "--format=java");
+
+ Throwable e = catchThrowable(command::doCall);
+
+ assertThat(e).isInstanceOf(NoSuchFileException.class);
+ // which the CLI ends with exit code 3
+ assertThat(new CliExecutionExceptionHandler(new
CamelJBangMain()).getExitCode(e)).isEqualTo(CliExitCode.NOT_FOUND);
+ }
+
+ @Test
+ void anInvalidRouteIsAFailure() throws Exception {
+ Path yaml = Files.writeString(dir.resolve("bad.camel.yaml"), "-
route:\n from:\n steps: []\n");
+
+ TransformRoute command = command(List.of(yaml.toString()),
"--format=xml");
+
+ // the route is wrong, not the CLI (which exited with 70 and the stack
trace)
+ assertThatThrownBy(command::doCall)
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.FAILURE))
+ .hasMessageStartingWith("Cannot transform the routes: ")
+ .hasMessageContaining("bad.camel.yaml");
+ }
+
+ @Test
+ void anInvalidRouteToJavaIsAFailure() throws Exception {
+ Path yaml = Files.writeString(dir.resolve("bad.camel.yaml"), "-
route:\n from:\n steps: []\n");
+
+ assertThat(command(List.of(yaml.toString()),
"--format=java").doCall()).isEqualTo(CliExitCode.FAILURE);
+
+ assertThat(printer.getStderr()).isNotEmpty();
+ }
+
+ private TransformRoute command(List<String> files, String... args) {
+ TransformRoute command = new TransformRoute(new
CamelJBangMain().withPrinter(printer));
+ CommandLine.populateCommand(command, args);
+ command.files = Arrays.asList(files.toArray(String[]::new));
+ return command;
+ }
+}