From 52993646c4573da838a908ad89edc3aef4332a25 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Tue, 7 Jul 2026 09:29:07 +0200 Subject: [PATCH 1/4] CAMEL-23928: camel-langchain4j-agent - Add AiServices builder customizer and tool-calling options to AgentConfiguration Co-Authored-By: Claude Opus 4.6 Signed-off-by: Claus Ibsen --- .../langchain4j/agent/api/AbstractAgent.java | 22 +++ .../agent/api/AgentConfiguration.java | 139 ++++++++++++++++++ .../agent/api/AgentConfigurationTest.java | 100 +++++++++++++ 3 files changed, 261 insertions(+) diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java index 0cccc29792b8f..bc837d6c2c42a 100644 --- a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java +++ b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AbstractAgent.java @@ -150,5 +150,27 @@ protected void configureBuilder(AiServices builder, ToolProvider toolProvider if (responseFormat != null) { builder.chatRequestTransformer(chatRequest -> chatRequest.toBuilder().responseFormat(responseFormat).build()); } + + // Tool calling options + if (configuration.getMaxToolCallingRoundTrips() > 0) { + builder.maxToolCallingRoundTrips(configuration.getMaxToolCallingRoundTrips()); + } + if (configuration.getHallucinatedToolNameStrategy() != null) { + builder.hallucinatedToolNameStrategy(configuration.getHallucinatedToolNameStrategy()); + } + if (configuration.getToolExecutionErrorHandler() != null) { + builder.toolExecutionErrorHandler(configuration.getToolExecutionErrorHandler()); + } + if (configuration.getToolArgumentsErrorHandler() != null) { + builder.toolArgumentsErrorHandler(configuration.getToolArgumentsErrorHandler()); + } + if (configuration.getCompensateOnToolErrors() != null) { + builder.compensateOnToolErrors(configuration.getCompensateOnToolErrors()); + } + + // Custom AiServices builder customizer (escape hatch for any builder option not directly exposed) + if (configuration.getAiServicesCustomizer() != null) { + configuration.getAiServicesCustomizer().accept(builder); + } } } diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentConfiguration.java b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentConfiguration.java index 65ea85617afb2..c813d7454ae53 100644 --- a/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentConfiguration.java +++ b/components/camel-ai/camel-langchain4j-agent-api/src/main/java/org/apache/camel/component/langchain4j/agent/api/AgentConfiguration.java @@ -21,13 +21,20 @@ import java.util.Collections; import java.util.List; import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Collectors; +import dev.langchain4j.agent.tool.ToolExecutionRequest; import dev.langchain4j.agent.tool.ToolSpecification; +import dev.langchain4j.data.message.ToolExecutionResultMessage; import dev.langchain4j.mcp.client.McpClient; import dev.langchain4j.memory.chat.ChatMemoryProvider; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.rag.RetrievalAugmentor; +import dev.langchain4j.service.AiServices; +import dev.langchain4j.service.tool.ToolArgumentsErrorHandler; +import dev.langchain4j.service.tool.ToolExecutionErrorHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,6 +74,12 @@ public class AgentConfiguration { private List customTools; // Custom LangChain4j tools private List mcpClients; // MCP clients for external tool integration private BiPredicate mcpToolProviderFilter; // Filter for MCP tools + private int maxToolCallingRoundTrips; // Max number of tool-calling round trips (0 = not set) + private Function hallucinatedToolNameStrategy; + private ToolExecutionErrorHandler toolExecutionErrorHandler; + private ToolArgumentsErrorHandler toolArgumentsErrorHandler; + private Boolean compensateOnToolErrors; + private Consumer> aiServicesCustomizer; /** * Gets the configured chat model. @@ -346,6 +359,132 @@ public AgentConfiguration withMcpToolProviderFilter(BiPredicate getHallucinatedToolNameStrategy() { + return hallucinatedToolNameStrategy; + } + + /** + * Sets the strategy for handling cases where the LLM hallucinates a tool name that does not exist. The function + * receives the invalid tool execution request and returns a result message to send back to the LLM. + * + * @param hallucinatedToolNameStrategy the strategy function for handling hallucinated tool names + * @return this configuration instance for method chaining + */ + public AgentConfiguration withHallucinatedToolNameStrategy( + Function hallucinatedToolNameStrategy) { + this.hallucinatedToolNameStrategy = hallucinatedToolNameStrategy; + return this; + } + + /** + * Gets the error handler for tool execution failures. + * + * @return the tool execution error handler, or {@code null} if not configured + */ + public ToolExecutionErrorHandler getToolExecutionErrorHandler() { + return toolExecutionErrorHandler; + } + + /** + * Sets the error handler invoked when a tool execution throws an exception. + * + * @param toolExecutionErrorHandler the handler for tool execution errors + * @return this configuration instance for method chaining + */ + public AgentConfiguration withToolExecutionErrorHandler(ToolExecutionErrorHandler toolExecutionErrorHandler) { + this.toolExecutionErrorHandler = toolExecutionErrorHandler; + return this; + } + + /** + * Gets the error handler for tool argument parsing failures. + * + * @return the tool arguments error handler, or {@code null} if not configured + */ + public ToolArgumentsErrorHandler getToolArgumentsErrorHandler() { + return toolArgumentsErrorHandler; + } + + /** + * Sets the error handler invoked when tool arguments cannot be parsed or validated. + * + * @param toolArgumentsErrorHandler the handler for tool argument errors + * @return this configuration instance for method chaining + */ + public AgentConfiguration withToolArgumentsErrorHandler(ToolArgumentsErrorHandler toolArgumentsErrorHandler) { + this.toolArgumentsErrorHandler = toolArgumentsErrorHandler; + return this; + } + + /** + * Gets whether tool error compensation is enabled. + * + * @return {@code true} if enabled, {@code false} if disabled, or {@code null} if not configured + */ + public Boolean getCompensateOnToolErrors() { + return compensateOnToolErrors; + } + + /** + * Sets whether the agent should attempt to compensate when tool execution errors occur by sending the error back to + * the LLM for recovery. + * + * @param compensateOnToolErrors {@code true} to enable compensation, {@code false} to disable + * @return this configuration instance for method chaining + */ + public AgentConfiguration withCompensateOnToolErrors(Boolean compensateOnToolErrors) { + this.compensateOnToolErrors = compensateOnToolErrors; + return this; + } + + /** + * Gets the custom AiServices builder customizer. + * + * @return the customizer, or {@code null} if not configured + */ + public Consumer> getAiServicesCustomizer() { + return aiServicesCustomizer; + } + + /** + * Sets a customizer callback that is invoked on the LangChain4j {@link AiServices} builder after all standard + * configuration has been applied but before {@code build()} is called. This provides an escape hatch for + * configuring any AiServices builder option that is not directly exposed on this configuration class. + * + * @param aiServicesCustomizer the customizer to apply to the AiServices builder + * @return this configuration instance for method chaining + */ + public AgentConfiguration withAiServicesCustomizer(Consumer> aiServicesCustomizer) { + this.aiServicesCustomizer = aiServicesCustomizer; + return this; + } + /** * Loads a guardrail class by its fully qualified name using reflection. * diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/AgentConfigurationTest.java b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/AgentConfigurationTest.java index 12a682d07ad03..6825b3720f8c8 100644 --- a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/AgentConfigurationTest.java +++ b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/AgentConfigurationTest.java @@ -18,11 +18,18 @@ import java.io.Serializable; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import dev.langchain4j.agent.tool.ToolExecutionRequest; +import dev.langchain4j.data.message.ToolExecutionResultMessage; +import dev.langchain4j.service.tool.ToolArgumentsErrorHandler; +import dev.langchain4j.service.tool.ToolExecutionErrorHandler; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; public class AgentConfigurationTest { @@ -249,4 +256,97 @@ public void testWithOutputGuardrailClassesArray_WithInvalidClasses() { assertNotNull(config.getOutputGuardrailClasses()); assertTrue(config.getOutputGuardrailClasses().isEmpty()); } + + // Tests for tool-calling options + + @Test + public void testMaxToolCallingRoundTrips() { + AgentConfiguration config = new AgentConfiguration(); + assertEquals(0, config.getMaxToolCallingRoundTrips()); + + AgentConfiguration result = config.withMaxToolCallingRoundTrips(10); + + assertSame(config, result); + assertEquals(10, config.getMaxToolCallingRoundTrips()); + } + + @Test + public void testHallucinatedToolNameStrategy() { + AgentConfiguration config = new AgentConfiguration(); + assertNull(config.getHallucinatedToolNameStrategy()); + + java.util.function.Function strategy + = request -> ToolExecutionResultMessage.from(request, "Unknown tool: " + request.name()); + AgentConfiguration result = config.withHallucinatedToolNameStrategy(strategy); + + assertSame(config, result); + assertSame(strategy, config.getHallucinatedToolNameStrategy()); + } + + @Test + public void testToolExecutionErrorHandler() { + AgentConfiguration config = new AgentConfiguration(); + assertNull(config.getToolExecutionErrorHandler()); + + ToolExecutionErrorHandler handler = (error, context) -> null; + AgentConfiguration result = config.withToolExecutionErrorHandler(handler); + + assertSame(config, result); + assertSame(handler, config.getToolExecutionErrorHandler()); + } + + @Test + public void testToolArgumentsErrorHandler() { + AgentConfiguration config = new AgentConfiguration(); + assertNull(config.getToolArgumentsErrorHandler()); + + ToolArgumentsErrorHandler handler = (error, context) -> null; + AgentConfiguration result = config.withToolArgumentsErrorHandler(handler); + + assertSame(config, result); + assertSame(handler, config.getToolArgumentsErrorHandler()); + } + + @Test + public void testCompensateOnToolErrors() { + AgentConfiguration config = new AgentConfiguration(); + assertNull(config.getCompensateOnToolErrors()); + + AgentConfiguration result = config.withCompensateOnToolErrors(true); + + assertSame(config, result); + assertTrue(config.getCompensateOnToolErrors()); + } + + @Test + public void testAiServicesCustomizer() { + AgentConfiguration config = new AgentConfiguration(); + assertNull(config.getAiServicesCustomizer()); + + AtomicBoolean invoked = new AtomicBoolean(false); + AgentConfiguration result = config.withAiServicesCustomizer(builder -> invoked.set(true)); + + assertSame(config, result); + assertNotNull(config.getAiServicesCustomizer()); + } + + @Test + public void testFluentChaining() { + ToolExecutionErrorHandler execHandler = (error, context) -> null; + ToolArgumentsErrorHandler argsHandler = (error, context) -> null; + + AgentConfiguration config = new AgentConfiguration() + .withMaxToolCallingRoundTrips(5) + .withToolExecutionErrorHandler(execHandler) + .withToolArgumentsErrorHandler(argsHandler) + .withCompensateOnToolErrors(true) + .withAiServicesCustomizer(builder -> { + }); + + assertEquals(5, config.getMaxToolCallingRoundTrips()); + assertSame(execHandler, config.getToolExecutionErrorHandler()); + assertSame(argsHandler, config.getToolArgumentsErrorHandler()); + assertTrue(config.getCompensateOnToolErrors()); + assertNotNull(config.getAiServicesCustomizer()); + } } From 0c96612510390947af3b698baf38635e0a4a0550 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Tue, 7 Jul 2026 10:45:57 +0200 Subject: [PATCH 2/4] CAMEL-23928: Fix FQCN usage in AgentConfigurationTest Co-Authored-By: Claude Opus 4.6 Signed-off-by: Claus Ibsen --- .../langchain4j/agent/api/AgentConfigurationTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/AgentConfigurationTest.java b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/AgentConfigurationTest.java index 6825b3720f8c8..297ac1ccc71ea 100644 --- a/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/AgentConfigurationTest.java +++ b/components/camel-ai/camel-langchain4j-agent-api/src/test/java/org/apache/camel/component/langchain4j/agent/api/AgentConfigurationTest.java @@ -19,6 +19,7 @@ import java.io.Serializable; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; import dev.langchain4j.agent.tool.ToolExecutionRequest; import dev.langchain4j.data.message.ToolExecutionResultMessage; @@ -275,7 +276,7 @@ public void testHallucinatedToolNameStrategy() { AgentConfiguration config = new AgentConfiguration(); assertNull(config.getHallucinatedToolNameStrategy()); - java.util.function.Function strategy + Function strategy = request -> ToolExecutionResultMessage.from(request, "Unknown tool: " + request.name()); AgentConfiguration result = config.withHallucinatedToolNameStrategy(strategy); From cc2c3b7236625ff1342ba439308f800cd253d8c1 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Tue, 7 Jul 2026 11:48:23 +0200 Subject: [PATCH 3/4] chore: reorganize bundled jbang examples into categorized folders Organize bundled examples into topic-based subfolders matching the companion reorganization of apache/camel-jbang-examples: - beginner/ - timer-log, cron-log, rest-api, routes, tui-hello-world, camel-1-tribute - eip/ - circuit-breaker, content-based-router (new), splitter (new), aggregator (new) - database/ - sql - language/ - groovy - observability/ - memory-leak, message-size, route-topology - transformation/ - xslt Three new EIP examples added: - content-based-router: choice/when routing by message content - splitter: split batch into individual messages - aggregator: collect messages into batches Catalog JSON updated with new paths for all 29 examples. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Claus Ibsen --- .../{ => beginner}/camel-1-tribute/README.md | 0 .../camel-1-tribute/application.properties | 0 .../camel-1-tribute/jms-to-file.camel.yaml | 0 .../{ => beginner}/cron-log/README.md | 0 .../cron-log/application.properties | 0 .../cron-log/cron-log.camel.yaml | 0 .../{ => beginner}/rest-api/README.md | 0 .../rest-api/application.properties | 0 .../rest-api/rest-api.camel.yaml | 0 .../{ => beginner}/routes/Greeter.java | 0 .../examples/{ => beginner}/routes/README.md | 0 .../routes/application.properties | 0 .../examples/{ => beginner}/routes/beans.yaml | 0 .../{ => beginner}/routes/routes.camel.yaml | 0 .../{ => beginner}/timer-log/README.md | 0 .../timer-log/application.properties | 0 .../timer-log/timer-log.camel.yaml | 0 .../{ => beginner}/tui-hello-world/README.md | 0 .../tui-hello-world/tui-hello-world.yaml | 0 .../examples/camel-jbang-example-catalog.json | 638 ++++++++++-------- .../examples/{ => database}/sql/README.md | 0 .../{ => database}/sql/application.properties | 0 .../{ => database}/sql/sql.camel.yaml | 0 .../examples/eip/aggregator/README.md | 47 ++ .../eip/aggregator/aggregator.camel.yaml | 37 + .../{ => eip}/circuit-breaker/README.md | 0 .../circuit-breaker/route.camel.yaml | 0 .../eip/content-based-router/README.md | 48 ++ .../content-based-router.camel.yaml | 45 ++ .../resources/examples/eip/splitter/README.md | 47 ++ .../examples/eip/splitter/splitter.camel.yaml | 22 + .../examples/{ => language}/groovy/README.md | 0 .../groovy/application.properties | 0 .../{ => language}/groovy/groovy.camel.yaml | 0 .../memory-leak/MemoryLeak.java | 0 .../{ => observability}/memory-leak/README.md | 0 .../message-size/README.md | 0 .../message-size/message-size.camel.yaml | 0 .../message-size/orders.camel.yaml | 0 .../route-topology/README.md | 0 .../route-topology/application.properties | 0 .../route-topology/route-topology.camel.yaml | 0 .../{ => transformation}/xslt/README.md | 0 .../xslt/consumer.camel.yaml | 0 .../xslt/input/account.xml | 0 .../{ => transformation}/xslt/stylesheet.xsl | 0 46 files changed, 592 insertions(+), 292 deletions(-) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/camel-1-tribute/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/camel-1-tribute/application.properties (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/camel-1-tribute/jms-to-file.camel.yaml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/cron-log/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/cron-log/application.properties (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/cron-log/cron-log.camel.yaml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/rest-api/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/rest-api/application.properties (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/rest-api/rest-api.camel.yaml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/routes/Greeter.java (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/routes/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/routes/application.properties (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/routes/beans.yaml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/routes/routes.camel.yaml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/timer-log/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/timer-log/application.properties (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/timer-log/timer-log.camel.yaml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/tui-hello-world/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => beginner}/tui-hello-world/tui-hello-world.yaml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => database}/sql/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => database}/sql/application.properties (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => database}/sql/sql.camel.yaml (100%) create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/aggregator/README.md create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/aggregator/aggregator.camel.yaml rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => eip}/circuit-breaker/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => eip}/circuit-breaker/route.camel.yaml (100%) create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/README.md create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/content-based-router.camel.yaml create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/splitter/README.md create mode 100644 dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/splitter/splitter.camel.yaml rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => language}/groovy/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => language}/groovy/application.properties (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => language}/groovy/groovy.camel.yaml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => observability}/memory-leak/MemoryLeak.java (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => observability}/memory-leak/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => observability}/message-size/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => observability}/message-size/message-size.camel.yaml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => observability}/message-size/orders.camel.yaml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => observability}/route-topology/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => observability}/route-topology/application.properties (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => observability}/route-topology/route-topology.camel.yaml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => transformation}/xslt/README.md (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => transformation}/xslt/consumer.camel.yaml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => transformation}/xslt/input/account.xml (100%) rename dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/{ => transformation}/xslt/stylesheet.xsl (100%) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-1-tribute/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/camel-1-tribute/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-1-tribute/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/camel-1-tribute/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-1-tribute/application.properties b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/camel-1-tribute/application.properties similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-1-tribute/application.properties rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/camel-1-tribute/application.properties diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-1-tribute/jms-to-file.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/camel-1-tribute/jms-to-file.camel.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-1-tribute/jms-to-file.camel.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/camel-1-tribute/jms-to-file.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/cron-log/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/cron-log/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/application.properties b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/cron-log/application.properties similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/application.properties rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/cron-log/application.properties diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/cron-log.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/cron-log/cron-log.camel.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/cron-log/cron-log.camel.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/cron-log/cron-log.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/rest-api/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/rest-api/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/application.properties b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/rest-api/application.properties similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/application.properties rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/rest-api/application.properties diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/rest-api.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/rest-api/rest-api.camel.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/rest-api/rest-api.camel.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/rest-api/rest-api.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/Greeter.java b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/routes/Greeter.java similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/Greeter.java rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/routes/Greeter.java diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/routes/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/routes/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/application.properties b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/routes/application.properties similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/application.properties rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/routes/application.properties diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/beans.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/routes/beans.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/beans.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/routes/beans.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/routes.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/routes/routes.camel.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/routes/routes.camel.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/routes/routes.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/timer-log/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/timer-log/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/application.properties b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/timer-log/application.properties similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/application.properties rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/timer-log/application.properties diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/timer-log.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/timer-log/timer-log.camel.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/timer-log/timer-log.camel.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/timer-log/timer-log.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/tui-hello-world/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/tui-hello-world/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/tui-hello-world/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/tui-hello-world/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/tui-hello-world/tui-hello-world.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/tui-hello-world/tui-hello-world.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/tui-hello-world/tui-hello-world.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/beginner/tui-hello-world/tui-hello-world.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json index a065576cc70f2..bd1affbb688b0 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/camel-jbang-example-catalog.json @@ -1,13 +1,14 @@ [ { - "name": "artemis", - "title": "Apache ActiveMQ Artemis", - "description": "Setup connection factory to a remote Apache ActiveMQ Artemis messaging broker", - "level": "intermediate", + "name": "ai/docling-langchain4j-rag", + "title": "Document Analysis with Docling and LangChain4j RAG", + "description": "RAG workflow combining Docling document conversion with LangChain4j and Ollama", + "level": "advanced", "tags": [ - "messaging", - "jms", - "artemis" + "ai", + "rag", + "langchain4j", + "docling" ], "bundled": false, "requiresDocker": true, @@ -15,59 +16,112 @@ "files": [ "README.md", "application.properties", - "consumer.camel.yaml", - "producer.camel.yaml" + "docling-langchain4j-rag.yaml", + "sample.md" ], "infraServices": [ - "artemis" + "docling", + "ollama" ] }, { - "name": "aws/aws-s3-event-based", - "title": "AWS S3 CDC", - "description": "Consume S3 events using EventBridge and SQS for change data capture", + "name": "ai/financial-doc-analyzer", + "title": "Financial Document Analyzer", + "description": "Automated financial document analysis with Docling, LangChain4j, and market data", "level": "advanced", "tags": [ - "aws", - "s3", - "sqs", - "eventbridge", - "cloud" + "ai", + "langchain4j", + "docling", + "finance" + ], + "bundled": false, + "requiresDocker": true, + "hasCitrusTests": false, + "files": [ + "README.md", + "application.properties", + "examples/banking-sector-brief.pdf", + "examples/magnificent-seven-update.pdf", + "examples/semiconductor-sector-analysis.pdf", + "examples/tesla-q3-2024.pdf", + "financial-doc-analyzer.yaml" + ], + "infraServices": [ + "docling", + "ollama" + ] + }, + { + "name": "ai/openai-pii-redaction", + "title": "OpenAI PII Redaction", + "description": "Redact personal identifiable information from text using OpenAI-compatible LLMs", + "level": "advanced", + "tags": [ + "ai", + "openai", + "privacy", + "security" ], "bundled": false, "requiresDocker": false, - "hasCitrusTests": true, + "hasCitrusTests": false, "files": [ "README.md", "application.properties", - "aws-s3-cdc-log.camel.yaml", - "example-file.txt", - "policy-queue.json", - "terraform/main.tf" + "pii-redaction.camel.yaml", + "pii.schema.json" ] }, { - "name": "aws/aws-sqs", - "title": "AWS SQS Sink", - "description": "Push messages to AWS SQS queue via HTTP service", + "name": "ai/smart-log-analyzer", + "title": "Smart Log Analyzer", + "description": "Intelligent observability correlating OpenTelemetry logs and traces with LLM analysis", "level": "advanced", "tags": [ - "aws", - "sqs", - "http", - "cloud" + "ai", + "observability", + "opentelemetry", + "logging" ], "bundled": false, "requiresDocker": false, "hasCitrusTests": true, "files": [ "README.md", - "application.properties", - "http-to-aws-sqs.camel.yaml" + "analyzer/application-dev.properties", + "analyzer/error-analyzer.camel.yaml", + "containers/caches/infinispan-events-config.json", + "containers/caches/infinispan-events-to-process-config.json", + "containers/otel-collector-config.yaml", + "correlator/application-dev.properties", + "correlator/correlated-log-schema.json", + "correlator/correlated-trace-schema.json", + "correlator/infinispan.camel.yaml", + "correlator/kafka-ca-cert.pem", + "correlator/kaoto-datamapper-4a94acc3.xsl", + "correlator/kaoto-datamapper-8f5bb2dd.xsl", + "correlator/logs-mapper.camel.yaml", + "correlator/otel-log-record-schema.json", + "correlator/otel-logs-schema.json", + "correlator/otel-span-schema.json", + "correlator/otel-traces-schema.json", + "correlator/traces-mapper.camel.yaml", + "first-iteration/analyzer.camel.yaml", + "first-iteration/application.properties", + "first-iteration/load-generator.camel.yaml", + "log-generator/agent.properties", + "log-generator/application-dev.properties", + "log-generator/log-generator.camel.yaml", + "log-generator/opentelemetry-javaagent.jar", + "ui-console/application-dev.properties", + "ui-console/index.html", + "ui-console/jms-file-storage.camel.yaml", + "ui-console/rest-api.camel.yaml" ] }, { - "name": "camel-1-tribute", + "name": "beginner/camel-1-tribute", "title": "Camel 1.0 Tribute", "description": "A tribute to the very first Apache Camel example from 2007 - JMS to File", "level": "beginner", @@ -91,31 +145,73 @@ ] }, { - "name": "circuit-breaker", - "title": "Circuit Breaker", - "description": "Use the circuit breaker EIP for fault tolerance", + "name": "beginner/cron-log", + "title": "Cron Log", + "description": "Scheduled task that logs every 5 seconds", "level": "beginner", "tags": [ - "eip", - "resilience" + "beginner", + "timer", + "cron", + "log" ], "bundled": true, "requiresDocker": false, "hasCitrusTests": false, "files": [ "README.md", - "route.camel.yaml" + "application.properties", + "cron-log.camel.yaml" ] }, { - "name": "cron-log", - "title": "Cron Log", - "description": "Scheduled task that logs every 5 seconds", + "name": "beginner/rest-api", + "title": "REST API", + "description": "REST API with hello endpoints", + "level": "beginner", + "tags": [ + "beginner", + "rest", + "http" + ], + "bundled": true, + "requiresDocker": false, + "hasCitrusTests": false, + "files": [ + "README.md", + "application.properties", + "rest-api.camel.yaml" + ] + }, + { + "name": "beginner/routes", + "title": "Routes", + "description": "Define routes in YAML with Java beans", + "level": "beginner", + "tags": [ + "beginner", + "yaml", + "bean" + ], + "bundled": true, + "requiresDocker": false, + "hasCitrusTests": false, + "files": [ + "Greeter.java", + "README.md", + "application.properties", + "beans.yaml", + "routes.camel.yaml" + ] + }, + { + "name": "beginner/timer-log", + "title": "Timer Log", + "description": "Simple timer that logs a hello message every second", "level": "beginner", "tags": [ "beginner", "timer", - "cron", "log" ], "bundled": true, @@ -124,88 +220,167 @@ "files": [ "README.md", "application.properties", - "cron-log.camel.yaml" + "timer-log.camel.yaml" ] }, { - "name": "docling-langchain4j-rag", - "title": "Document Analysis with Docling and LangChain4j RAG", - "description": "RAG workflow combining Docling document conversion with LangChain4j and Ollama", + "name": "beginner/tui-hello-world", + "title": "TUI Hello World", + "description": "Say hello via TUI Send Message (F2) or CLI", + "level": "beginner", + "tags": [ + "beginner", + "direct", + "send", + "tui" + ], + "bundled": true, + "requiresDocker": false, + "hasCitrusTests": false, + "files": [ + "README.md", + "tui-hello-world.yaml" + ] + }, + { + "name": "cloud/aws-s3-event-based", + "title": "AWS S3 CDC", + "description": "Consume S3 events using EventBridge and SQS for change data capture", "level": "advanced", "tags": [ - "ai", - "rag", - "langchain4j", - "docling" + "aws", + "s3", + "sqs", + "eventbridge", + "cloud" ], "bundled": false, - "requiresDocker": true, - "hasCitrusTests": false, + "requiresDocker": false, + "hasCitrusTests": true, "files": [ "README.md", "application.properties", - "docling-langchain4j-rag.yaml", - "sample.md" - ], - "infraServices": [ - "docling", - "ollama" + "aws-s3-cdc-log.camel.yaml", + "example-file.txt", + "policy-queue.json", + "terraform/main.tf" ] }, { - "name": "financial-doc-analyzer", - "title": "Financial Document Analyzer", - "description": "Automated financial document analysis with Docling, LangChain4j, and market data", + "name": "cloud/aws-sqs", + "title": "AWS SQS Sink", + "description": "Push messages to AWS SQS queue via HTTP service", "level": "advanced", "tags": [ - "ai", - "langchain4j", - "docling", - "finance" + "aws", + "sqs", + "http", + "cloud" ], "bundled": false, - "requiresDocker": true, - "hasCitrusTests": false, + "requiresDocker": false, + "hasCitrusTests": true, "files": [ "README.md", "application.properties", - "examples/banking-sector-brief.pdf", - "examples/magnificent-seven-update.pdf", - "examples/semiconductor-sector-analysis.pdf", - "examples/tesla-q3-2024.pdf", - "financial-doc-analyzer.yaml" - ], - "infraServices": [ - "docling", - "ollama" + "http-to-aws-sqs.camel.yaml" ] }, { - "name": "ftp", - "title": "ActiveMQ to FTP", - "description": "Integrate ActiveMQ messaging with an FTP server", + "name": "database/sql", + "title": "SQL Database", + "description": "Use a SQL database with Camel and Postgres", "level": "intermediate", "tags": [ - "messaging", - "ftp", - "activemq" + "database", + "sql", + "postgres" ], - "bundled": false, + "bundled": true, "requiresDocker": true, - "hasCitrusTests": true, + "hasCitrusTests": false, "files": [ "README.md", "application.properties", - "ftp.camel.yaml", - "jbang.properties" + "sql.camel.yaml" ], "infraServices": [ - "artemis", - "ftp" + "postgres" + ] + }, + { + "name": "eip/aggregator", + "title": "Aggregator", + "description": "Collect individual messages into a batch using the Aggregator EIP", + "level": "beginner", + "tags": [ + "eip", + "aggregator", + "batch" + ], + "bundled": true, + "requiresDocker": false, + "hasCitrusTests": false, + "files": [ + "README.md", + "aggregator.camel.yaml" + ] + }, + { + "name": "eip/circuit-breaker", + "title": "Circuit Breaker", + "description": "Use the circuit breaker EIP for fault tolerance", + "level": "beginner", + "tags": [ + "eip", + "resilience" + ], + "bundled": true, + "requiresDocker": false, + "hasCitrusTests": false, + "files": [ + "README.md", + "route.camel.yaml" + ] + }, + { + "name": "eip/content-based-router", + "title": "Content Based Router", + "description": "Route messages to different destinations based on message content using the Choice EIP", + "level": "beginner", + "tags": [ + "eip", + "choice", + "routing" + ], + "bundled": true, + "requiresDocker": false, + "hasCitrusTests": false, + "files": [ + "README.md", + "content-based-router.camel.yaml" + ] + }, + { + "name": "eip/splitter", + "title": "Splitter", + "description": "Split a batch of items into individual messages for processing", + "level": "beginner", + "tags": [ + "eip", + "splitter", + "batch" + ], + "bundled": true, + "requiresDocker": false, + "hasCitrusTests": false, + "files": [ + "README.md", + "splitter.camel.yaml" ] }, { - "name": "groovy", + "name": "language/groovy", "title": "Groovy", "description": "Use Groovy with extra dependencies and content-based routing", "level": "beginner", @@ -224,15 +399,14 @@ ] }, { - "name": "keycloak-introspection-rest", - "title": "Keycloak Token Introspection REST API", - "description": "Secure REST APIs with Keycloak OAuth 2.0 token introspection", - "level": "advanced", + "name": "messaging/artemis", + "title": "Apache ActiveMQ Artemis", + "description": "Setup connection factory to a remote Apache ActiveMQ Artemis messaging broker", + "level": "intermediate", "tags": [ - "security", - "keycloak", - "rest", - "oauth" + "messaging", + "jms", + "artemis" ], "bundled": false, "requiresDocker": true, @@ -240,37 +414,63 @@ "files": [ "README.md", "application.properties", - "rest-api.camel.yaml" + "consumer.camel.yaml", + "producer.camel.yaml" ], "infraServices": [ - "keycloak" + "artemis" ] }, { - "name": "keycloak-security-rest", - "title": "Keycloak Security REST API", - "description": "Secure REST APIs with Keycloak authentication and authorization", - "level": "advanced", + "name": "messaging/ftp", + "title": "ActiveMQ to FTP", + "description": "Integrate ActiveMQ messaging with an FTP server", + "level": "intermediate", "tags": [ - "security", - "keycloak", - "rest", - "oauth" + "messaging", + "ftp", + "activemq" ], "bundled": false, "requiresDocker": true, - "hasCitrusTests": false, + "hasCitrusTests": true, "files": [ "README.md", "application.properties", - "rest-api.camel.yaml" + "ftp.camel.yaml", + "jbang.properties" ], "infraServices": [ - "keycloak" + "artemis", + "ftp" + ] + }, + { + "name": "messaging/mqtt", + "title": "MQTT", + "description": "Receive MQTT events from an external MQTT broker", + "level": "intermediate", + "tags": [ + "messaging", + "mqtt", + "iot" + ], + "bundled": false, + "requiresDocker": true, + "hasCitrusTests": true, + "files": [ + "README.md", + "application.properties", + "infra/mosquitto.conf", + "mqtt.camel.yaml", + "start.sh" + ], + "infraServices": [ + "mosquitto" ] }, { - "name": "memory-leak", + "name": "observability/memory-leak", "title": "Memory Leak", "description": "Simulates a memory leak for testing JFR Old Object Sample diagnostics", "level": "beginner", @@ -289,7 +489,7 @@ ] }, { - "name": "message-size", + "name": "observability/message-size", "title": "Message Size", "description": "Track message body and header sizes per endpoint", "level": "beginner", @@ -307,52 +507,30 @@ ] }, { - "name": "mqtt", - "title": "MQTT", - "description": "Receive MQTT events from an external MQTT broker", + "name": "observability/route-topology", + "title": "Route Topology", + "description": "Demonstrates inter-route topology with triggers, shared routes, and external systems", "level": "intermediate", "tags": [ - "messaging", - "mqtt", - "iot" + "topology", + "direct", + "kafka", + "timer" ], - "bundled": false, + "bundled": true, "requiresDocker": true, - "hasCitrusTests": true, + "hasCitrusTests": false, "files": [ "README.md", "application.properties", - "infra/mosquitto.conf", - "mqtt.camel.yaml", - "start.sh" + "route-topology.camel.yaml" ], "infraServices": [ - "mosquitto" - ] - }, - { - "name": "openai/pii-redaction", - "title": "OpenAI PII Redaction", - "description": "Redact personal identifiable information from text using OpenAI-compatible LLMs", - "level": "advanced", - "tags": [ - "ai", - "openai", - "privacy", - "security" - ], - "bundled": false, - "requiresDocker": false, - "hasCitrusTests": false, - "files": [ - "README.md", - "application.properties", - "pii-redaction.camel.yaml", - "pii.schema.json" + "kafka" ] }, { - "name": "openapi/client", + "name": "rest/openapi-client", "title": "OpenAPI Client", "description": "REST client generated from an OpenAPI specification", "level": "intermediate", @@ -373,7 +551,7 @@ ] }, { - "name": "openapi/server", + "name": "rest/openapi-server", "title": "OpenAPI Server", "description": "REST service implemented from an OpenAPI specification", "level": "intermediate", @@ -394,177 +572,53 @@ ] }, { - "name": "rest-api", - "title": "REST API", - "description": "REST API with hello endpoints", - "level": "beginner", + "name": "security/keycloak-introspection-rest", + "title": "Keycloak Token Introspection REST API", + "description": "Secure REST APIs with Keycloak OAuth 2.0 token introspection", + "level": "advanced", "tags": [ - "beginner", + "security", + "keycloak", "rest", - "http" - ], - "bundled": true, - "requiresDocker": false, - "hasCitrusTests": false, - "files": [ - "README.md", - "application.properties", - "rest-api.camel.yaml" - ] - }, - { - "name": "route-topology", - "title": "Route Topology", - "description": "Demonstrates inter-route topology with triggers, shared routes, and external systems", - "level": "intermediate", - "tags": [ - "topology", - "direct", - "kafka", - "timer" + "oauth" ], - "bundled": true, + "bundled": false, "requiresDocker": true, "hasCitrusTests": false, "files": [ "README.md", "application.properties", - "route-topology.camel.yaml" + "rest-api.camel.yaml" ], "infraServices": [ - "kafka" - ] - }, - { - "name": "routes", - "title": "Routes", - "description": "Define routes in YAML with Java beans", - "level": "beginner", - "tags": [ - "beginner", - "yaml", - "bean" - ], - "bundled": true, - "requiresDocker": false, - "hasCitrusTests": false, - "files": [ - "Greeter.java", - "README.md", - "application.properties", - "beans.yaml", - "routes.camel.yaml" + "keycloak" ] }, { - "name": "smart-log-analyzer", - "title": "Smart Log Analyzer", - "description": "Intelligent observability correlating OpenTelemetry logs and traces with LLM analysis", + "name": "security/keycloak-security-rest", + "title": "Keycloak Security REST API", + "description": "Secure REST APIs with Keycloak authentication and authorization", "level": "advanced", "tags": [ - "ai", - "observability", - "opentelemetry", - "logging" + "security", + "keycloak", + "rest", + "oauth" ], "bundled": false, - "requiresDocker": false, - "hasCitrusTests": true, - "files": [ - "README.md", - "analyzer/application-dev.properties", - "analyzer/error-analyzer.camel.yaml", - "containers/caches/infinispan-events-config.json", - "containers/caches/infinispan-events-to-process-config.json", - "containers/otel-collector-config.yaml", - "correlator/application-dev.properties", - "correlator/correlated-log-schema.json", - "correlator/correlated-trace-schema.json", - "correlator/infinispan.camel.yaml", - "correlator/kafka-ca-cert.pem", - "correlator/kaoto-datamapper-4a94acc3.xsl", - "correlator/kaoto-datamapper-8f5bb2dd.xsl", - "correlator/logs-mapper.camel.yaml", - "correlator/otel-log-record-schema.json", - "correlator/otel-logs-schema.json", - "correlator/otel-span-schema.json", - "correlator/otel-traces-schema.json", - "correlator/traces-mapper.camel.yaml", - "first-iteration/analyzer.camel.yaml", - "first-iteration/application.properties", - "first-iteration/load-generator.camel.yaml", - "log-generator/agent.properties", - "log-generator/application-dev.properties", - "log-generator/log-generator.camel.yaml", - "log-generator/opentelemetry-javaagent.jar", - "ui-console/application-dev.properties", - "ui-console/index.html", - "ui-console/jms-file-storage.camel.yaml", - "ui-console/rest-api.camel.yaml" - ] - }, - { - "name": "sql", - "title": "SQL Database", - "description": "Use a SQL database with Camel and Postgres", - "level": "intermediate", - "tags": [ - "database", - "sql", - "postgres" - ], - "bundled": true, "requiresDocker": true, "hasCitrusTests": false, "files": [ "README.md", "application.properties", - "sql.camel.yaml" + "rest-api.camel.yaml" ], "infraServices": [ - "postgres" - ] - }, - { - "name": "timer-log", - "title": "Timer Log", - "description": "Simple timer that logs a hello message every second", - "level": "beginner", - "tags": [ - "beginner", - "timer", - "log" - ], - "bundled": true, - "requiresDocker": false, - "hasCitrusTests": false, - "files": [ - "README.md", - "application.properties", - "timer-log.camel.yaml" - ] - }, - { - "name": "tui-hello-world", - "title": "TUI Hello World", - "description": "Say hello via TUI Send Message (F2) or CLI", - "level": "beginner", - "tags": [ - "beginner", - "direct", - "send", - "tui" - ], - "bundled": true, - "requiresDocker": false, - "hasCitrusTests": false, - "files": [ - "README.md", - "tui-hello-world.yaml" + "keycloak" ] }, { - "name": "xslt", + "name": "transformation/xslt", "title": "XSLT Transformation", "description": "Basic XML transformation using XSLT style sheets", "level": "beginner", diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/database/sql/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/database/sql/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/application.properties b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/database/sql/application.properties similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/application.properties rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/database/sql/application.properties diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/sql.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/database/sql/sql.camel.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/sql/sql.camel.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/database/sql/sql.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/aggregator/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/aggregator/README.md new file mode 100644 index 0000000000000..2f66737101feb --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/aggregator/README.md @@ -0,0 +1,47 @@ +## Aggregator + +This example demonstrates the Aggregator EIP. + +Individual order messages are collected into batches of 5 using the Aggregator EIP. +The `StringAggregationStrategy` joins message bodies with a comma delimiter. + +A timer generates one order per second. The aggregator collects 5 orders before +releasing the batch, which is then logged as a single combined message. + +### Install JBang + +First install JBang according to https://www.jbang.dev + +When JBang is installed then you should be able to run from a shell: + +```sh +$ jbang --version +``` + +This will output the version of JBang. + +To run this example you can either install Camel on JBang via: + +```sh +$ jbang app install camel@apache/camel +``` + +Which allows to run Camel CLI with `camel` as shown below. + +### How to run + +You can run this example using: + +```sh +$ camel run * +``` + +### Help and contributions + +If you hit any problem using Camel or have some feedback, then please +[let us know](https://camel.apache.org/community/support/). + +We also love contributors, so +[get involved](https://camel.apache.org/community/contributing/) :-) + +The Camel riders! diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/aggregator/aggregator.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/aggregator/aggregator.camel.yaml new file mode 100644 index 0000000000000..45a750c03a9bc --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/aggregator/aggregator.camel.yaml @@ -0,0 +1,37 @@ +- beans: + - name: myAggregationStrategy + type: "#class:org.apache.camel.processor.aggregate.StringAggregationStrategy" + properties: + delimiter: ", " + +- route: + id: order-generator + from: + uri: timer + parameters: + timerName: orders + period: 1000 + steps: + - setBody: + expression: + simple: + expression: "Order-${exchangeProperty.CamelTimerCounter}" + - log: + message: "New: ${body}" + - to: + uri: direct:aggregate + +- route: + id: order-aggregator + from: + uri: direct:aggregate + steps: + - aggregate: + aggregationStrategy: "#bean:myAggregationStrategy" + correlationExpression: + constant: + expression: "true" + completionSize: 5 + steps: + - log: + message: "Batch of 5: ${body}" diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/circuit-breaker/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/circuit-breaker/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/route.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/circuit-breaker/route.camel.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/circuit-breaker/route.camel.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/circuit-breaker/route.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/README.md new file mode 100644 index 0000000000000..d030d8dde2add --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/README.md @@ -0,0 +1,48 @@ +## Content Based Router + +This example demonstrates the Content Based Router EIP. + +A simulated temperature sensor generates random readings between 0 and 40. +The route uses the `choice` EIP to classify each reading: + +- **Hot** (>= 30): logged as an alert +- **Normal** (15-29): logged as normal +- **Cold** (< 15): logged as an alert + +### Install JBang + +First install JBang according to https://www.jbang.dev + +When JBang is installed then you should be able to run from a shell: + +```sh +$ jbang --version +``` + +This will output the version of JBang. + +To run this example you can either install Camel on JBang via: + +```sh +$ jbang app install camel@apache/camel +``` + +Which allows to run Camel CLI with `camel` as shown below. + +### How to run + +You can run this example using: + +```sh +$ camel run * +``` + +### Help and contributions + +If you hit any problem using Camel or have some feedback, then please +[let us know](https://camel.apache.org/community/support/). + +We also love contributors, so +[get involved](https://camel.apache.org/community/contributing/) :-) + +The Camel riders! diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/content-based-router.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/content-based-router.camel.yaml new file mode 100644 index 0000000000000..698c2b1c39dce --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/content-based-router/content-based-router.camel.yaml @@ -0,0 +1,45 @@ +- route: + id: sensor + from: + uri: timer + parameters: + timerName: sensor + period: 1000 + steps: + - setBody: + expression: + simple: + expression: "${random(0,40)}" + - convertBodyTo: + type: int + - choice: + when: + - simple: + expression: "${body} >= 30" + steps: + - setHeader: + name: level + expression: + constant: + expression: hot + - log: + message: "Hot alert: ${body} C" + - simple: + expression: "${body} >= 15" + steps: + - setHeader: + name: level + expression: + constant: + expression: normal + - log: + message: "Normal: ${body} C" + otherwise: + steps: + - setHeader: + name: level + expression: + constant: + expression: cold + - log: + message: "Cold alert: ${body} C" diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/splitter/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/splitter/README.md new file mode 100644 index 0000000000000..ea673ccfe3aa8 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/splitter/README.md @@ -0,0 +1,47 @@ +## Splitter + +This example demonstrates the Splitter EIP. + +A batch of comma-separated product names is split into individual messages. +Each item is logged with its split index. + +The splitter processes each part of the message independently, which is useful +for breaking down bulk data into individual records for downstream processing. + +### Install JBang + +First install JBang according to https://www.jbang.dev + +When JBang is installed then you should be able to run from a shell: + +```sh +$ jbang --version +``` + +This will output the version of JBang. + +To run this example you can either install Camel on JBang via: + +```sh +$ jbang app install camel@apache/camel +``` + +Which allows to run Camel CLI with `camel` as shown below. + +### How to run + +You can run this example using: + +```sh +$ camel run * +``` + +### Help and contributions + +If you hit any problem using Camel or have some feedback, then please +[let us know](https://camel.apache.org/community/support/). + +We also love contributors, so +[get involved](https://camel.apache.org/community/contributing/) :-) + +The Camel riders! diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/splitter/splitter.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/splitter/splitter.camel.yaml new file mode 100644 index 0000000000000..b87b5b4b4d56b --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/eip/splitter/splitter.camel.yaml @@ -0,0 +1,22 @@ +- route: + id: order-splitter + from: + uri: timer + parameters: + timerName: orders + period: 5000 + repeatCount: 3 + steps: + - setBody: + expression: + constant: + expression: "Laptop,Phone,Tablet,Monitor,Keyboard" + - log: + message: "Received batch: ${body}" + - split: + expression: + tokenize: + token: "," + steps: + - log: + message: "Processing item ${headers.CamelSplitIndex}: ${body}" diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/groovy/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/language/groovy/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/groovy/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/language/groovy/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/groovy/application.properties b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/language/groovy/application.properties similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/groovy/application.properties rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/language/groovy/application.properties diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/groovy/groovy.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/language/groovy/groovy.camel.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/groovy/groovy.camel.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/language/groovy/groovy.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/memory-leak/MemoryLeak.java b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/memory-leak/MemoryLeak.java similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/memory-leak/MemoryLeak.java rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/memory-leak/MemoryLeak.java diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/memory-leak/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/memory-leak/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/memory-leak/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/memory-leak/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/message-size/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/message-size/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/message-size/message-size.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/message-size.camel.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/message-size/message-size.camel.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/message-size.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/message-size/orders.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/orders.camel.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/message-size/orders.camel.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/message-size/orders.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/route-topology/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/route-topology/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/application.properties b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/route-topology/application.properties similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/application.properties rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/route-topology/application.properties diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/route-topology.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/route-topology/route-topology.camel.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/route-topology/route-topology.camel.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/observability/route-topology/route-topology.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/README.md b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transformation/xslt/README.md similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/README.md rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transformation/xslt/README.md diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/consumer.camel.yaml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transformation/xslt/consumer.camel.yaml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/consumer.camel.yaml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transformation/xslt/consumer.camel.yaml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/input/account.xml b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transformation/xslt/input/account.xml similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/input/account.xml rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transformation/xslt/input/account.xml diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/stylesheet.xsl b/dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transformation/xslt/stylesheet.xsl similarity index 100% rename from dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/xslt/stylesheet.xsl rename to dsl/camel-jbang/camel-jbang-core/src/main/resources/examples/transformation/xslt/stylesheet.xsl From 4ecbaf98ec3fb5709b5e0f2d4c056c06afa81632 Mon Sep 17 00:00:00 2001 From: Claus Ibsen Date: Tue, 7 Jul 2026 14:31:18 +0200 Subject: [PATCH 4/4] chore: add category sub-grouping to example listing in CLI and TUI Add category-based organization to the example browser in both CLI (camel run --example) and TUI. Examples are now sub-grouped by category within each difficulty level. The TUI uses a folder-based navigation model with ".." to go back. Fix tests for the new categorized names. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Claus Ibsen --- .../camel/dsl/jbang/core/commands/Run.java | 23 +- .../dsl/jbang/core/common/ExampleHelper.java | 21 ++ .../dsl/jbang/core/commands/RunTest.java | 2 +- .../jbang/core/common/ExampleHelperTest.java | 36 +- .../commands/tui/ExampleBrowserPopup.java | 341 +++++++++++------- 5 files changed, 275 insertions(+), 148 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java index 99603ac96d449..d8ecbe8a82439 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java @@ -411,13 +411,26 @@ private int listExamples(String filter) { if (entries.isEmpty()) { continue; } - entries.sort(Comparator.comparing(e -> e.getString("name"))); + String levelName = group.getKey(); + entries.sort(Comparator.comparing((JsonObject e) -> { + String cat = ExampleHelper.getCategory(e); + return cat.equals(levelName) ? "" : cat; + }).thenComparing(e -> e.getString("name"))); printer().println(); - printer().println(group.getKey().substring(0, 1).toUpperCase() + group.getKey().substring(1) + ":"); - printer().printf(" %-30s %s%n", "NAME", "DESCRIPTION"); - printer().printf(" %-30s %s%n", "----", "-----------"); + String levelLabel = levelName.substring(0, 1).toUpperCase() + levelName.substring(1) + ":"; + printer().println(levelLabel); + printer().println("=".repeat(levelLabel.length())); + String currentCategory = null; for (JsonObject entry : entries) { - String eName = entry.getString("name"); + String category = ExampleHelper.getCategory(entry); + if (!category.equals(currentCategory)) { + currentCategory = category; + if (!category.equals(levelName)) { + printer().println(); + printer().println(" " + ExampleHelper.formatCategory(category) + ":"); + } + } + String eName = ExampleHelper.getShortName(entry); String desc = entry.getString("description"); StringBuilder icons = new StringBuilder(); if (ExampleHelper.isBundled(entry)) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java index f7674f50268f9..d1821c946c2f1 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java @@ -122,6 +122,27 @@ private static boolean matches(JsonObject entry, String filter) { return false; } + public static String getCategory(JsonObject entry) { + String name = entry.getString("name"); + int slash = name != null ? name.indexOf('/') : -1; + return slash > 0 ? name.substring(0, slash) : ""; + } + + public static String formatCategory(String category) { + return switch (category) { + case "ai" -> "AI"; + case "eip" -> "EIP"; + case "rest" -> "REST"; + default -> category.substring(0, 1).toUpperCase() + category.substring(1); + }; + } + + public static String getShortName(JsonObject entry) { + String name = entry.getString("name"); + int slash = name != null ? name.indexOf('/') : -1; + return slash > 0 ? name.substring(slash + 1) : name != null ? name : ""; + } + public static boolean isBundled(JsonObject entry) { Boolean bundled = entry.getBoolean("bundled"); return bundled != null && bundled; diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java index 707dffb184de5..bf762ec037204 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/RunTest.java @@ -81,7 +81,7 @@ public void shouldRejectUnknownExample() throws Exception { @Test public void shouldSuggestSimilarExample() throws Exception { Run command = new Run(new CamelJBangMain().withPrinter(printer)); - command.example = "circuit-brake"; + command.example = "eip/circuit-brake"; int exit = command.doCall(); Assertions.assertEquals(1, exit); diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java index 09a8ce533be96..965c3417fa03e 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java @@ -36,7 +36,7 @@ void shouldLoadCatalog() { @Test void shouldFindExample() { List catalog = ExampleHelper.loadCatalog(); - JsonObject entry = ExampleHelper.findExample(catalog, "circuit-breaker"); + JsonObject entry = ExampleHelper.findExample(catalog, "eip/circuit-breaker"); assertNotNull(entry); assertEquals("Circuit Breaker", entry.getString("title")); } @@ -66,7 +66,7 @@ void shouldFilterByName() { List catalog = ExampleHelper.loadCatalog(); List filtered = ExampleHelper.filterExamples(catalog, "mqtt"); assertEquals(1, filtered.size()); - assertEquals("mqtt", filtered.get(0).getString("name")); + assertEquals("messaging/mqtt", filtered.get(0).getString("name")); } @Test @@ -79,27 +79,27 @@ void shouldReturnAllWhenFilterEmpty() { @Test void shouldDetectBundled() { List catalog = ExampleHelper.loadCatalog(); - JsonObject circuitBreaker = ExampleHelper.findExample(catalog, "circuit-breaker"); + JsonObject circuitBreaker = ExampleHelper.findExample(catalog, "eip/circuit-breaker"); assertTrue(ExampleHelper.isBundled(circuitBreaker)); - JsonObject mqtt = ExampleHelper.findExample(catalog, "mqtt"); + JsonObject mqtt = ExampleHelper.findExample(catalog, "messaging/mqtt"); assertFalse(ExampleHelper.isBundled(mqtt)); } @Test void shouldDetectDocker() { List catalog = ExampleHelper.loadCatalog(); - JsonObject mqtt = ExampleHelper.findExample(catalog, "mqtt"); + JsonObject mqtt = ExampleHelper.findExample(catalog, "messaging/mqtt"); assertTrue(ExampleHelper.requiresDocker(mqtt)); - JsonObject circuitBreaker = ExampleHelper.findExample(catalog, "circuit-breaker"); + JsonObject circuitBreaker = ExampleHelper.findExample(catalog, "eip/circuit-breaker"); assertFalse(ExampleHelper.requiresDocker(circuitBreaker)); } @Test void shouldGetFiles() { List catalog = ExampleHelper.loadCatalog(); - JsonObject routes = ExampleHelper.findExample(catalog, "routes"); + JsonObject routes = ExampleHelper.findExample(catalog, "beginner/routes"); List files = ExampleHelper.getFiles(routes); assertTrue(files.contains("routes.camel.yaml")); assertTrue(files.contains("Greeter.java")); @@ -109,7 +109,7 @@ void shouldGetFiles() { @Test void shouldExtractBundledExample() throws Exception { List catalog = ExampleHelper.loadCatalog(); - JsonObject entry = ExampleHelper.findExample(catalog, "circuit-breaker"); + JsonObject entry = ExampleHelper.findExample(catalog, "eip/circuit-breaker"); Path tempDir = ExampleHelper.extractBundledExample(entry); assertTrue(Files.exists(tempDir.resolve("route.camel.yaml"))); @@ -120,7 +120,7 @@ void shouldExtractBundledExample() throws Exception { @Test void shouldExtractBundledExampleWithSubdirectory() throws Exception { List catalog = ExampleHelper.loadCatalog(); - JsonObject entry = ExampleHelper.findExample(catalog, "xslt"); + JsonObject entry = ExampleHelper.findExample(catalog, "transformation/xslt"); Path tempDir = ExampleHelper.extractBundledExample(entry); assertTrue(Files.exists(tempDir.resolve("consumer.camel.yaml"))); @@ -131,26 +131,26 @@ void shouldExtractBundledExampleWithSubdirectory() throws Exception { @Test void shouldGetGithubUrl() { List catalog = ExampleHelper.loadCatalog(); - JsonObject entry = ExampleHelper.findExample(catalog, "mqtt"); + JsonObject entry = ExampleHelper.findExample(catalog, "messaging/mqtt"); String url = ExampleHelper.getGithubUrl(entry); - assertEquals("https://github.com/apache/camel-jbang-examples/tree/main/mqtt", url); + assertEquals("https://github.com/apache/camel-jbang-examples/tree/main/messaging/mqtt", url); } @Test void shouldGetGithubUrlForNestedExample() { List catalog = ExampleHelper.loadCatalog(); - JsonObject entry = ExampleHelper.findExample(catalog, "aws/aws-sqs"); + JsonObject entry = ExampleHelper.findExample(catalog, "cloud/aws-sqs"); String url = ExampleHelper.getGithubUrl(entry); - assertEquals("https://github.com/apache/camel-jbang-examples/tree/main/aws/aws-sqs", url); + assertEquals("https://github.com/apache/camel-jbang-examples/tree/main/cloud/aws-sqs", url); } @Test void shouldDetectCitrusTests() { List catalog = ExampleHelper.loadCatalog(); - JsonObject mqtt = ExampleHelper.findExample(catalog, "mqtt"); + JsonObject mqtt = ExampleHelper.findExample(catalog, "messaging/mqtt"); assertTrue(ExampleHelper.hasCitrusTests(mqtt)); - JsonObject circuitBreaker = ExampleHelper.findExample(catalog, "circuit-breaker"); + JsonObject circuitBreaker = ExampleHelper.findExample(catalog, "eip/circuit-breaker"); assertFalse(ExampleHelper.hasCitrusTests(circuitBreaker)); } @@ -158,8 +158,8 @@ void shouldDetectCitrusTests() { void shouldGetExampleNames() { List catalog = ExampleHelper.loadCatalog(); List names = ExampleHelper.getExampleNames(catalog); - assertTrue(names.contains("circuit-breaker")); - assertTrue(names.contains("mqtt")); - assertTrue(names.contains("aws/aws-sqs")); + assertTrue(names.contains("eip/circuit-breaker")); + assertTrue(names.contains("messaging/mqtt")); + assertTrue(names.contains("cloud/aws-sqs")); } } diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java index 9baded6112415..335a203653f48 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java @@ -19,7 +19,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.function.BiConsumer; import dev.tamboui.layout.Rect; @@ -46,6 +48,8 @@ class ExampleBrowserPopup { + private static final String BACK_MARKER = "__BACK__"; + private boolean visible; private final ListState listState = new ListState(); private Rect popupRect; @@ -53,6 +57,11 @@ class ExampleBrowserPopup { private List catalog; private JsonObject selectedExample; + private String currentFolder; + private String currentLevel; + private int savedSelection; + private List itemData; + private final DocViewerPopup docViewerPopup; private final LaunchManager launchManager; private Runnable burstCallback; @@ -92,6 +101,7 @@ void open() { notify("No examples found", true); return; } + currentFolder = null; visible = true; listState.select(1); } @@ -102,7 +112,11 @@ void close() { boolean handleKeyEvent(KeyEvent ke) { if (ke.isCancel()) { - close(); + if (currentFolder != null) { + navigateBack(); + } else { + close(); + } return true; } if (ke.isUp()) { @@ -122,7 +136,7 @@ boolean handleKeyEvent(KeyEvent ke) { return true; } if (ke.isChar('r')) { - launchSelected(); + activateSelected(false); return true; } if (ke.isChar('d')) { @@ -130,7 +144,7 @@ boolean handleKeyEvent(KeyEvent ke) { return true; } if (ke.isConfirm()) { - openNameInput(); + activateSelected(true); return true; } return true; @@ -184,7 +198,8 @@ void render(Frame frame, Rect area) { return; } int popupW = Math.min(100, area.width() - 4); - int popupH = Math.min(catalog.size() + 10, Math.min(22, area.height() - 4)); + int visibleItems = Math.max(10, catalog.size() + 10); + int popupH = Math.min(visibleItems, Math.min(22, area.height() - 4)); int x = area.left() + Math.max(0, (area.width() - popupW) / 2); int y = area.top() + 2; Rect popup = new Rect(x, y, Math.min(popupW, area.width()), Math.min(popupH, area.height() - 2)); @@ -193,6 +208,9 @@ void render(Frame frame, Rect area) { frame.renderWidget(Clear.INSTANCE, popup); List items = buildListItems(popupW - 4); + String title = currentFolder != null + ? " " + ExampleHelper.formatCategory(currentFolder) + " " + : " Run an Example (" + catalog.size() + ") "; ListWidget list = ListWidget.builder() .items(items.toArray(ListItem[]::new)) .highlightStyle(Theme.selectionBg()) @@ -200,7 +218,7 @@ void render(Frame frame, Rect area) { .scrollMode(ScrollMode.AUTO_SCROLL) .block(Block.builder() .borderType(BorderType.ROUNDED).borders(Borders.ALL) - .title(" Run an Example (" + catalog.size() + ") ") + .title(title) .build()) .build(); frame.renderStatefulWidget(list, popup, listState); @@ -209,28 +227,31 @@ void render(Frame frame, Rect area) { void renderFooter(List spans) { TuiHelper.hint(spans, "↑↓", "navigate"); TuiHelper.hint(spans, "r", "run"); - TuiHelper.hint(spans, "Enter", "run..."); + TuiHelper.hint(spans, "Enter", currentFolder != null ? "run..." : "open/run..."); TuiHelper.hint(spans, "d", "docs"); TuiHelper.hintLast(spans, "Esc", "back"); } SelectionContext getSelectionContext() { - if (!visible || catalog == null) { + if (!visible || itemData == null) { return null; } List items = new ArrayList<>(); - String currentLevel = null; - for (JsonObject ex : catalog) { - String level = ex.getStringOrDefault("level", "beginner"); - if (!level.equals(currentLevel)) { - currentLevel = level; - items.add("── " + TuiHelper.capitalize(level) + " ──"); + for (Object data : itemData) { + if (data == null) { + items.add("──"); + } else if (BACK_MARKER.equals(data)) { + items.add(".."); + } else if (data instanceof String folder) { + int slash = folder.indexOf('/'); + String cat = slash > 0 ? folder.substring(slash + 1) : folder; + items.add(TuiIcons.FOLDER + " " + ExampleHelper.formatCategory(cat)); + } else if (data instanceof JsonObject ex) { + items.add(ExampleHelper.getShortName(ex)); } - items.add(ex.getStringOrDefault("name", "")); } - int total = countListItems(); Integer sel = listState.selected(); - return new SelectionContext("list", items, sel != null ? sel : -1, total, "Examples"); + return new SelectionContext("list", items, sel != null ? sel : -1, itemData.size(), "Examples"); } void doLaunch(String exampleName, String displayName, List extraArgs) { @@ -266,15 +287,40 @@ void startMissingInfraAndDefer( // ---- Private ---- - private void launchSelected() { + private void activateSelected(boolean withOptions) { Integer sel = listState.selected(); if (sel == null || isSeparatorIndex(sel)) { return; } - JsonObject example = getExampleAtListIndex(sel); - if (example == null) { - return; + Object data = itemData.get(sel); + if (BACK_MARKER.equals(data)) { + navigateBack(); + } else if (data instanceof String folder) { + enterFolder(folder); + } else if (data instanceof JsonObject example) { + if (withOptions) { + openNameInput(example); + } else { + launchExample(example); + } } + } + + private void enterFolder(String folder) { + Integer sel = listState.selected(); + savedSelection = sel != null ? sel : 0; + int slash = folder.indexOf('/'); + currentLevel = folder.substring(0, slash); + currentFolder = folder.substring(slash + 1); + listState.select(0); + } + + private void navigateBack() { + currentFolder = null; + listState.select(savedSelection); + } + + private void launchExample(JsonObject example) { String exampleName = example.getStringOrDefault("name", ""); visible = false; @@ -292,15 +338,7 @@ private void launchSelected() { doLaunch(exampleName, exampleName, List.of()); } - private void openNameInput() { - Integer sel = listState.selected(); - if (sel == null || isSeparatorIndex(sel)) { - return; - } - JsonObject example = getExampleAtListIndex(sel); - if (example == null) { - return; - } + private void openNameInput(JsonObject example) { selectedExample = example; visible = false; if (onNameInputRequest != null) { @@ -310,11 +348,11 @@ private void openNameInput() { private void loadDocFromExample() { Integer sel = listState.selected(); - if (sel == null || isSeparatorIndex(sel)) { + if (sel == null || sel >= itemData.size()) { return; } - JsonObject example = getExampleAtListIndex(sel); - if (example == null) { + Object data = itemData.get(sel); + if (!(data instanceof JsonObject example)) { return; } String name = example.getStringOrDefault("name", ""); @@ -345,10 +383,10 @@ private void loadDocFromExample() { } private void navigate(int direction) { - if (catalog == null || catalog.isEmpty()) { + if (itemData == null || itemData.isEmpty()) { return; } - int totalItems = countListItems(); + int totalItems = itemData.size(); Integer current = listState.selected(); if (current == null) { current = 0; @@ -375,127 +413,182 @@ private void navigate(int direction) { listState.select(next); } - private int countListItems() { - if (catalog == null) { - return 0; - } - int count = 0; - String currentLevel = null; - for (JsonObject ex : catalog) { - String level = ex.getStringOrDefault("level", "beginner"); - if (!level.equals(currentLevel)) { - currentLevel = level; - count++; - } - count++; - } - return count + 2; - } - private boolean isSeparatorIndex(int index) { - if (catalog == null) { - return false; - } - int pos = 0; - String currentLevel = null; - for (JsonObject ex : catalog) { - String level = ex.getStringOrDefault("level", "beginner"); - if (!level.equals(currentLevel)) { - currentLevel = level; - if (pos == index) { - return true; - } - pos++; - } - if (pos == index) { - return false; - } - pos++; + if (itemData == null || index < 0 || index >= itemData.size()) { + return true; } - return true; + return itemData.get(index) == null; } - private JsonObject getExampleAtListIndex(int index) { - if (catalog == null) { - return null; - } - int pos = 0; - String currentLevel = null; - for (JsonObject ex : catalog) { - String level = ex.getStringOrDefault("level", "beginner"); - if (!level.equals(currentLevel)) { - currentLevel = level; - pos++; - } - if (pos == index) { - return ex; - } - pos++; + private List buildListItems(int width) { + if (currentFolder != null) { + return buildFolderItems(width); } - return null; + return buildTopLevelItems(width); } - private List buildListItems(int width) { + private List buildTopLevelItems(int width) { List items = new ArrayList<>(); List heights = new ArrayList<>(); - String currentLevel = null; + List data = new ArrayList<>(); + + Map> levelGroups = new LinkedHashMap<>(); + for (String level : new String[] { "beginner", "intermediate", "advanced" }) { + levelGroups.put(level, new ArrayList<>()); + } for (JsonObject ex : catalog) { - String level = ex.getStringOrDefault("level", "beginner"); - if (!level.equals(currentLevel)) { - currentLevel = level; - String header = "── " + TuiHelper.capitalize(level) + " ──"; - items.add(ListItem.from(header).style(Style.EMPTY.dim())); - heights.add(1); + String level = ex.getStringOrDefault("level", "intermediate"); + levelGroups.computeIfAbsent(level, k -> new ArrayList<>()).add(ex); + } + + boolean firstLevel = true; + for (Map.Entry> group : levelGroups.entrySet()) { + List entries = group.getValue(); + if (entries.isEmpty()) { + continue; } - String name = ex.getStringOrDefault("name", ""); - String desc = ex.getStringOrDefault("description", ""); - boolean docker = ExampleHelper.requiresDocker(ex); - boolean bundled = ExampleHelper.isBundled(ex); - boolean citrus = ExampleHelper.hasCitrusTests(ex); - boolean infra = !ExampleHelper.getInfraServices(ex).isEmpty(); - - String icons = (bundled ? TuiIcons.BUNDLED : TuiIcons.ONLINE) + (docker ? TuiIcons.DOCKER : " ") - + (infra ? TuiIcons.INFRA : " ") + (citrus ? TuiIcons.CITRUS : " "); - int nameCol = Math.min(30, width / 3); - String padded = String.format("%-" + nameCol + "s", TuiHelper.truncate(name, nameCol)); - String prefix = " " + icons + " " + padded + " "; - int descCol = Math.max(10, width - prefix.length()); - - Style style = bundled ? Style.EMPTY : Style.EMPTY.dim(); - if (desc.length() <= descCol) { - items.add(ListItem.from(prefix + desc).style(style)); - heights.add(1); - } else { - String indent = " ".repeat(prefix.length()); - List lines = new ArrayList<>(); - List wrapped = TuiHelper.wrapWords(desc, descCol); - lines.add(Line.from(prefix + wrapped.get(0))); - for (int w = 1; w < wrapped.size(); w++) { - lines.add(Line.from(indent + wrapped.get(w))); + String levelName = group.getKey(); + + String label = " " + TuiHelper.capitalize(levelName) + " "; + int pad = Math.max(0, (width - label.length()) / 2); + String header = "─".repeat(pad) + label + "─".repeat(pad); + items.add(ListItem.from(header).style(Style.EMPTY.dim())); + heights.add(1); + data.add(null); + + entries.sort((a, b) -> { + String catA = ExampleHelper.getCategory(a); + String catB = ExampleHelper.getCategory(b); + String sortA = catA.equals(levelName) ? "" : catA; + String sortB = catB.equals(levelName) ? "" : catB; + int cc = sortA.compareTo(sortB); + return cc != 0 ? cc : a.getString("name").compareTo(b.getString("name")); + }); + + Map> categoryGroups = new LinkedHashMap<>(); + for (JsonObject ex : entries) { + String cat = ExampleHelper.getCategory(ex); + categoryGroups.computeIfAbsent(cat, k -> new ArrayList<>()).add(ex); + } + + for (Map.Entry> catGroup : categoryGroups.entrySet()) { + String category = catGroup.getKey(); + List catEntries = catGroup.getValue(); + + if (category.equals(levelName)) { + for (JsonObject ex : catEntries) { + addExampleItem(items, heights, data, ex, width); + } + } else { + String folderLabel = " " + TuiIcons.FOLDER + " " + ExampleHelper.formatCategory(category) + + " (" + catEntries.size() + ")"; + items.add(ListItem.from(folderLabel)); + heights.add(1); + data.add(levelName + "/" + category); } - items.add(ListItem.from(Text.from(lines.toArray(Line[]::new))).style(style)); - heights.add(wrapped.size()); } } + items.add(ListItem.from("")); heights.add(1); + data.add(null); items.add(ListItem.from(" " + TuiIcons.BUNDLED + " = bundled " + TuiIcons.ONLINE + " = online " + TuiIcons.DOCKER + " = Docker " + TuiIcons.INFRA + " = infra services " + TuiIcons.CITRUS + " = Citrus tests") .style(Style.EMPTY.dim())); heights.add(1); + data.add(null); + + this.itemHeights = heights.stream().mapToInt(Integer::intValue).toArray(); + this.itemData = data; + return items; + } + + private List buildFolderItems(int width) { + List items = new ArrayList<>(); + List heights = new ArrayList<>(); + List data = new ArrayList<>(); + + items.add(ListItem.from(" ..").style(Style.EMPTY.dim())); + heights.add(1); + data.add(BACK_MARKER); + + for (JsonObject ex : catalog) { + String level = ex.getStringOrDefault("level", "intermediate"); + if (currentFolder.equals(ExampleHelper.getCategory(ex)) && currentLevel.equals(level)) { + addExampleItem(items, heights, data, ex, width); + } + } + this.itemHeights = heights.stream().mapToInt(Integer::intValue).toArray(); + this.itemData = data; return items; } + private void addExampleItem( + List items, List heights, List data, + JsonObject ex, int width) { + String name = ExampleHelper.getShortName(ex); + String desc = ex.getStringOrDefault("description", ""); + boolean docker = ExampleHelper.requiresDocker(ex); + boolean bundled = ExampleHelper.isBundled(ex); + boolean citrus = ExampleHelper.hasCitrusTests(ex); + boolean infra = !ExampleHelper.getInfraServices(ex).isEmpty(); + + String icons = (bundled ? TuiIcons.BUNDLED : TuiIcons.ONLINE) + (docker ? TuiIcons.DOCKER : " ") + + (infra ? TuiIcons.INFRA : " ") + (citrus ? TuiIcons.CITRUS : " "); + int nameCol = Math.min(30, width / 3); + String padded = String.format("%-" + nameCol + "s", TuiHelper.truncate(name, nameCol)); + String prefix = " " + icons + " " + padded + " "; + int descCol = Math.max(10, width - prefix.length()); + + Style style = bundled ? Style.EMPTY : Style.EMPTY.dim(); + if (desc.length() <= descCol) { + items.add(ListItem.from(prefix + desc).style(style)); + heights.add(1); + } else { + String indent = " ".repeat(prefix.length()); + List lines = new ArrayList<>(); + List wrapped = TuiHelper.wrapWords(desc, descCol); + lines.add(Line.from(prefix + wrapped.get(0))); + for (int w = 1; w < wrapped.size(); w++) { + lines.add(Line.from(indent + wrapped.get(w))); + } + items.add(ListItem.from(Text.from(lines.toArray(Line[]::new))).style(style)); + heights.add(wrapped.size()); + } + data.add(ex); + } + + private int folderExampleCount(String folder) { + int count = 0; + for (JsonObject ex : catalog) { + String level = ex.getStringOrDefault("level", "intermediate"); + if (folder.equals(ExampleHelper.getCategory(ex)) && currentLevel.equals(level)) { + count++; + } + } + return count; + } + private static List loadAndSortExamples() { List list = ExampleHelper.loadCatalog(); list.sort((a, b) -> { - int la = levelOrder(a.getStringOrDefault("level", "beginner")); - int lb = levelOrder(b.getStringOrDefault("level", "beginner")); + String levelA = a.getStringOrDefault("level", "beginner"); + String levelB = b.getStringOrDefault("level", "beginner"); + int la = levelOrder(levelA); + int lb = levelOrder(levelB); if (la != lb) { return Integer.compare(la, lb); } + String catA = ExampleHelper.getCategory(a); + String catB = ExampleHelper.getCategory(b); + String sortCatA = catA.equals(levelA) ? "" : catA; + String sortCatB = catB.equals(levelB) ? "" : catB; + int cc = sortCatA.compareTo(sortCatB); + if (cc != 0) { + return cc; + } return a.getStringOrDefault("name", "").compareTo(b.getStringOrDefault("name", "")); }); return list;