This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new de7124e5aa5a chore: reorganize bundled jbang examples with category
sub-grouping (#24492)
de7124e5aa5a is described below
commit de7124e5aa5a25b3db5998e5ece4aee6b9ea7a83
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Jul 7 16:21:01 2026 +0200
chore: reorganize bundled jbang examples with category sub-grouping (#24492)
* CAMEL-23928: camel-langchain4j-agent - Add AiServices builder customizer
and tool-calling options to AgentConfiguration
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* CAMEL-23928: Fix FQCN usage in AgentConfigurationTest
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* 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 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
* 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 <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
.../langchain4j/agent/api/AbstractAgent.java | 22 +
.../langchain4j/agent/api/AgentConfiguration.java | 139 +++++
.../agent/api/AgentConfigurationTest.java | 101 ++++
.../apache/camel/dsl/jbang/core/commands/Run.java | 23 +-
.../camel/dsl/jbang/core/common/ExampleHelper.java | 21 +
.../{ => beginner}/camel-1-tribute/README.md | 0
.../camel-1-tribute/application.properties | 0
.../camel-1-tribute/jms-to-file.camel.yaml | 0
.../examples/{ => beginner}/cron-log/README.md | 0
.../{ => beginner}/cron-log/application.properties | 0
.../{ => beginner}/cron-log/cron-log.camel.yaml | 0
.../examples/{ => beginner}/rest-api/README.md | 0
.../{ => beginner}/rest-api/application.properties | 0
.../{ => beginner}/rest-api/rest-api.camel.yaml | 0
.../examples/{ => beginner}/routes/Greeter.java | 0
.../examples/{ => beginner}/routes/README.md | 0
.../{ => beginner}/routes/application.properties | 0
.../examples/{ => beginner}/routes/beans.yaml | 0
.../{ => beginner}/routes/routes.camel.yaml | 0
.../examples/{ => beginner}/timer-log/README.md | 0
.../timer-log/application.properties | 0
.../{ => beginner}/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
.../examples/{ => database}/sql/sql.camel.yaml | 0
.../resources/examples/eip/aggregator/README.md | 47 ++
.../examples/eip/aggregator/aggregator.camel.yaml | 37 ++
.../examples/{ => eip}/circuit-breaker/README.md | 0
.../{ => eip}/circuit-breaker/route.camel.yaml | 0
.../examples/eip/content-based-router/README.md | 48 ++
.../content-based-router.camel.yaml | 45 ++
.../main/resources/examples/eip/splitter/README.md | 47 ++
.../examples/eip/splitter/splitter.camel.yaml | 22 +
.../examples/{ => language}/groovy/README.md | 0
.../{ => language}/groovy/application.properties | 0
.../{ => language}/groovy/groovy.camel.yaml | 0
.../memory-leak/MemoryLeak.java | 0
.../{ => observability}/memory-leak/README.md | 0
.../{ => observability}/message-size/README.md | 0
.../message-size/message-size.camel.yaml | 0
.../message-size/orders.camel.yaml | 0
.../{ => observability}/route-topology/README.md | 0
.../route-topology/application.properties | 0
.../route-topology/route-topology.camel.yaml | 0
.../examples/{ => transformation}/xslt/README.md | 0
.../{ => transformation}/xslt/consumer.camel.yaml | 0
.../{ => transformation}/xslt/input/account.xml | 0
.../{ => transformation}/xslt/stylesheet.xsl | 0
.../camel/dsl/jbang/core/commands/RunTest.java | 2 +-
.../dsl/jbang/core/common/ExampleHelperTest.java | 36 +-
.../core/commands/tui/ExampleBrowserPopup.java | 341 +++++++----
54 files changed, 1129 insertions(+), 440 deletions(-)
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 0cccc29792b8..bc837d6c2c42 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 @@ public abstract class AbstractAgent<S> implements Agent {
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 65ea85617afb..c813d7454ae5 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.Arrays;
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<Object> customTools; // Custom LangChain4j tools
private List<McpClient> mcpClients; // MCP clients for external tool
integration
private BiPredicate<McpClient, ToolSpecification> mcpToolProviderFilter;
// Filter for MCP tools
+ private int maxToolCallingRoundTrips; // Max number of tool-calling round
trips (0 = not set)
+ private Function<ToolExecutionRequest, ToolExecutionResultMessage>
hallucinatedToolNameStrategy;
+ private ToolExecutionErrorHandler toolExecutionErrorHandler;
+ private ToolArgumentsErrorHandler toolArgumentsErrorHandler;
+ private Boolean compensateOnToolErrors;
+ private Consumer<AiServices<?>> aiServicesCustomizer;
/**
* Gets the configured chat model.
@@ -346,6 +359,132 @@ public class AgentConfiguration {
return this;
}
+ /**
+ * Gets the maximum number of tool-calling round trips allowed.
+ *
+ * @return the maximum round trips, or 0 if not configured
+ */
+ public int getMaxToolCallingRoundTrips() {
+ return maxToolCallingRoundTrips;
+ }
+
+ /**
+ * Sets the maximum number of tool-calling round trips the LLM can make
per request. This limits how many times the
+ * model can invoke tools before a final response must be produced.
+ *
+ * @param maxToolCallingRoundTrips the maximum number of round trips
(must be positive)
+ * @return this configuration instance for method
chaining
+ */
+ public AgentConfiguration withMaxToolCallingRoundTrips(int
maxToolCallingRoundTrips) {
+ this.maxToolCallingRoundTrips = maxToolCallingRoundTrips;
+ return this;
+ }
+
+ /**
+ * Gets the strategy for handling hallucinated (non-existent) tool names.
+ *
+ * @return the hallucinated tool name strategy, or {@code null} if not
configured
+ */
+ public Function<ToolExecutionRequest, ToolExecutionResultMessage>
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<ToolExecutionRequest, ToolExecutionResultMessage>
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<AiServices<?>> 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<AiServices<?>>
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 12a682d07ad0..297ac1ccc71e 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,19 @@ package org.apache.camel.component.langchain4j.agent.api;
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;
+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 +257,97 @@ public class AgentConfigurationTest {
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());
+
+ Function<ToolExecutionRequest, ToolExecutionResultMessage> 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());
+ }
}
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 99603ac96d44..d8ecbe8a8243 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 @@ public class Run extends CamelCommand {
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 f7674f50268f..d1821c946c2f 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 @@ public final class ExampleHelper {
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/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 a065576cc70f..bd1affbb688b 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 000000000000..2f66737101fe
--- /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 000000000000..45a750c03a9b
--- /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 000000000000..d030d8dde2ad
--- /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 000000000000..698c2b1c39dc
--- /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 000000000000..ea673ccfe3aa
--- /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 000000000000..b87b5b4b4d56
--- /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
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 707dffb184de..bf762ec03720 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 @@ class RunTest extends CamelCommandBaseTestSupport {
@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 09a8ce533be9..965c3417fa03 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 @@ class ExampleHelperTest {
@Test
void shouldFindExample() {
List<JsonObject> 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 @@ class ExampleHelperTest {
List<JsonObject> catalog = ExampleHelper.loadCatalog();
List<JsonObject> 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 @@ class ExampleHelperTest {
@Test
void shouldDetectBundled() {
List<JsonObject> 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<JsonObject> 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<JsonObject> catalog = ExampleHelper.loadCatalog();
- JsonObject routes = ExampleHelper.findExample(catalog, "routes");
+ JsonObject routes = ExampleHelper.findExample(catalog,
"beginner/routes");
List<String> files = ExampleHelper.getFiles(routes);
assertTrue(files.contains("routes.camel.yaml"));
assertTrue(files.contains("Greeter.java"));
@@ -109,7 +109,7 @@ class ExampleHelperTest {
@Test
void shouldExtractBundledExample() throws Exception {
List<JsonObject> 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 @@ class ExampleHelperTest {
@Test
void shouldExtractBundledExampleWithSubdirectory() throws Exception {
List<JsonObject> 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 @@ class ExampleHelperTest {
@Test
void shouldGetGithubUrl() {
List<JsonObject> 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<JsonObject> 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<JsonObject> 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 @@ class ExampleHelperTest {
void shouldGetExampleNames() {
List<JsonObject> catalog = ExampleHelper.loadCatalog();
List<String> 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 9baded611241..335a203653f4 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 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
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 @@ import org.apache.camel.util.json.JsonObject;
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<JsonObject> catalog;
private JsonObject selectedExample;
+ private String currentFolder;
+ private String currentLevel;
+ private int savedSelection;
+ private List<Object> itemData;
+
private final DocViewerPopup docViewerPopup;
private final LaunchManager launchManager;
private Runnable burstCallback;
@@ -92,6 +101,7 @@ class ExampleBrowserPopup {
notify("No examples found", true);
return;
}
+ currentFolder = null;
visible = true;
listState.select(1);
}
@@ -102,7 +112,11 @@ class ExampleBrowserPopup {
boolean handleKeyEvent(KeyEvent ke) {
if (ke.isCancel()) {
- close();
+ if (currentFolder != null) {
+ navigateBack();
+ } else {
+ close();
+ }
return true;
}
if (ke.isUp()) {
@@ -122,7 +136,7 @@ class ExampleBrowserPopup {
return true;
}
if (ke.isChar('r')) {
- launchSelected();
+ activateSelected(false);
return true;
}
if (ke.isChar('d')) {
@@ -130,7 +144,7 @@ class ExampleBrowserPopup {
return true;
}
if (ke.isConfirm()) {
- openNameInput();
+ activateSelected(true);
return true;
}
return true;
@@ -184,7 +198,8 @@ class ExampleBrowserPopup {
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 @@ class ExampleBrowserPopup {
frame.renderWidget(Clear.INSTANCE, popup);
List<ListItem> 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 @@ class ExampleBrowserPopup {
.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 @@ class ExampleBrowserPopup {
void renderFooter(List<Span> 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<String> 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<String>
extraArgs) {
@@ -266,15 +287,40 @@ class ExampleBrowserPopup {
// ---- 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 @@ class ExampleBrowserPopup {
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 @@ class ExampleBrowserPopup {
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 @@ class ExampleBrowserPopup {
}
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 @@ class ExampleBrowserPopup {
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<ListItem> buildListItems(int width) {
+ if (currentFolder != null) {
+ return buildFolderItems(width);
}
- return null;
+ return buildTopLevelItems(width);
}
- private List<ListItem> buildListItems(int width) {
+ private List<ListItem> buildTopLevelItems(int width) {
List<ListItem> items = new ArrayList<>();
List<Integer> heights = new ArrayList<>();
- String currentLevel = null;
+ List<Object> data = new ArrayList<>();
+
+ Map<String, List<JsonObject>> 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<String, List<JsonObject>> group :
levelGroups.entrySet()) {
+ List<JsonObject> 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<Line> lines = new ArrayList<>();
- List<String> 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<String, List<JsonObject>> categoryGroups = new
LinkedHashMap<>();
+ for (JsonObject ex : entries) {
+ String cat = ExampleHelper.getCategory(ex);
+ categoryGroups.computeIfAbsent(cat, k -> new
ArrayList<>()).add(ex);
+ }
+
+ for (Map.Entry<String, List<JsonObject>> catGroup :
categoryGroups.entrySet()) {
+ String category = catGroup.getKey();
+ List<JsonObject> 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<ListItem> buildFolderItems(int width) {
+ List<ListItem> items = new ArrayList<>();
+ List<Integer> heights = new ArrayList<>();
+ List<Object> 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<ListItem> items, List<Integer> heights, List<Object> 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<Line> lines = new ArrayList<>();
+ List<String> 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<JsonObject> loadAndSortExamples() {
List<JsonObject> 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;