This is an automated email from the ASF dual-hosted git repository. luigidemasi pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
commit db66a09cb474129119205c28b64618c358af8ce3 Author: Luigi De Masi <[email protected]> AuthorDate: Sat Oct 10 08:14:43 2026 +0200 CAMEL-25489: Address semantic CLI review feedback Group inspection and evaluation under camel semantic, with get as the default. Preserve text usage help and failed lookup exit codes, reject unsupported runtime versions early, and use tables for human output. Share request-file handling with the TUI and existing RuntimeHelper clients. Add regression coverage, help examples, and updated generated command and language documentation. Co-authored-by: OpenAI Codex <[email protected]> Signed-off-by: Luigi De Masi <[email protected]> --- .../camel/catalog/docs/semantic-language.adoc | 3 + .../src/main/docs/semantic-language.adoc | 3 + .../modules/ROOT/pages/camel-jbang-managing.adoc | 35 ++--- .../ROOT/pages/jbang-commands/camel-jbang-cmd.adoc | 1 - .../pages/jbang-commands/camel-jbang-commands.adoc | 1 + .../ROOT/pages/jbang-commands/camel-jbang-get.adoc | 1 - ...valuate.adoc => camel-jbang-semantic-eval.adoc} | 4 +- ...semantic.adoc => camel-jbang-semantic-get.adoc} | 4 +- ...get-semantic.adoc => camel-jbang-semantic.adoc} | 14 +- .../META-INF/camel-jbang-commands-metadata.json | 5 +- .../dsl/jbang/core/commands/CamelJBangMain.java | 5 +- .../jbang/core/commands/action/CamelSemantic.java | 51 +++++--- .../commands/action/SemanticActionCommand.java | 46 +++---- .../core/commands/action/SemanticEvaluate.java | 12 +- .../jbang/core/commands/action/SemanticGet.java | 33 +++++ .../MissingPluginParameterExceptionHandler.java | 8 +- .../exceptionhandler/UsageErrorHandler.java | 26 ++++ .../camel/dsl/jbang/core/common/RuntimeHelper.java | 76 ++++++++--- .../core/commands/action/SemanticCommandTest.java | 143 ++++++++++++++++++-- .../dsl/jbang/core/common/RuntimeHelperTest.java | 145 +++++++++++++++++++++ .../jbang/core/commands/tui/MonitorContext.java | 16 +-- 21 files changed, 514 insertions(+), 118 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc index 9ebafdf2564e..ca271702ccc3 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc @@ -910,6 +910,9 @@ include::partial$language-options.adoc[] == Developer consoles +Use `camel semantic` to inspect definitions and `camel semantic eval` to evaluate them from the +xref:manual::camel-jbang-managing.adoc#_semantic_definitions_and_evaluation[Camel CLI]. + `semantic-metadata` exposes definitions and expert contracts without performing inference. `semantic-evaluate` evaluates a named definition against a sample exchange or calls an expert operation directly. It uses the configured provider and its normal validation, without sending diff --git a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc index 9ebafdf2564e..ca271702ccc3 100644 --- a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc +++ b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc @@ -910,6 +910,9 @@ include::partial$language-options.adoc[] == Developer consoles +Use `camel semantic` to inspect definitions and `camel semantic eval` to evaluate them from the +xref:manual::camel-jbang-managing.adoc#_semantic_definitions_and_evaluation[Camel CLI]. + `semantic-metadata` exposes definitions and expert contracts without performing inference. `semantic-evaluate` evaluates a named definition against a sample exchange or calls an expert operation directly. It uses the configured provider and its normal validation, without sending diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc index 06bf53b9e529..ae6f8f8413c1 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-managing.adoc @@ -283,27 +283,30 @@ TIP: Separate multiple IDs with commas: `--id=log1,log2,setBody2`. The CLI can inspect and try the running application's xref:next@components:languages:semantic-language.adoc[semantic definitions and experts]. -The application must include `camel-semantic` with the `semantic-metadata` and -`semantic-evaluate` developer consoles and have the local CLI connector enabled. +The application must run Camel 4.23 or newer, include `camel-semantic` with the +`semantic-metadata` and `semantic-evaluate` developer consoles, and have the local +CLI connector enabled. These commands use the connector; enabling the HTTP console is unnecessary. [source,bash] ---- -camel get semantic my-app -camel get semantic my-app --json -camel get semantic my-app --expert=guard +camel semantic my-app +camel semantic get my-app +camel semantic get my-app --json +camel semantic get my-app --expert=guard ---- -`get semantic` lists definition names, experts, operations, state selectors, result types -and resolution errors, followed by the known experts and default expert. `--expert` +`camel semantic` defaults to `camel semantic get`. The text output uses tables listing +definition names, experts, operations, state selectors, result types and resolution +errors, followed by the known experts and default expert. `--expert` shows one expert's operations, input and result semantics, and parameter contract. -Inspection does not perform inference. +Inspection does not perform inference. Use `--json` for machine-readable output. Evaluate a named definition with a sample exchange: [source,bash] ---- -camel cmd semantic-evaluate my-app --evaluation=safe \ +camel semantic eval my-app --evaluation=safe \ --body='Please summarize this message' --header=subject=example \ --variable=language=en --json ---- @@ -312,14 +315,14 @@ The definition's state selector reads the supplied body, headers and exchange-lo variables. Variables with repository prefixes such as `global:` are rejected. The sample is evaluated directly; it is not sent through the application's routes. -Alternatively, invoke an expert operation directly. Use `get semantic --expert` to +Alternatively, invoke an expert operation directly. Use `semantic get --expert` to find the operation names and parameters supported by that expert: [source,bash] ---- -camel cmd semantic-evaluate my-app --expert=guard --operation=detect \ +camel semantic eval my-app --expert=guard --operation=detect \ --input='Please summarize this message' --json -camel cmd semantic-evaluate my-app --expert=grader --operation=score \ +camel semantic eval my-app --expert=grader --operation=score \ --input='Sample answer' --parameter='criteria=json:["poor","good","excellent"]' --json ---- @@ -343,6 +346,8 @@ native inference may continue. Configure provider-specific request timeouts as w Both commands accept an integration name or PID. With no name they select the only running integration; ambiguous selection fails and lists the matching PIDs. +Known versions older than 4.23 fail immediately with exit code 3. If the runtime version +is unavailable, the CLI attempts the request and applies the normal timeout. Each request has its own reply file, so concurrent commands do not consume each other's results. @@ -362,11 +367,11 @@ result fields, such as `error` and `elapsedMillis`. Diagnostics go to stderr. | 2 | Invalid command arguments or sample values. | 3 | No matching integration, ambiguous selection, unavailable semantic tooling or expert operations. | 4 | No reply before the CLI timeout. -| 70 | Unexpected internal error; stderr includes the stack trace. +| 70 | Unexpected local CLI error; stderr includes the stack trace. |=== -See xref:jbang-commands/camel-jbang-get-semantic.adoc[camel get semantic] and -xref:jbang-commands/camel-jbang-cmd-semantic-evaluate.adoc[camel cmd semantic-evaluate] +See xref:jbang-commands/camel-jbang-semantic-get.adoc[camel semantic get] and +xref:jbang-commands/camel-jbang-semantic-eval.adoc[camel semantic eval] for the complete option reference. == Developer Console diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc index 943602405d14..e6edad675139 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd.adoc @@ -35,7 +35,6 @@ camel cmd [options] | xref:jbang-commands/camel-jbang-cmd-route-dump.adoc[route-dump] | Dump Camel route in XML, YAML, or Java DSL format | xref:jbang-commands/camel-jbang-cmd-route-structure.adoc[route-structure] | Dump Camel route structure | xref:jbang-commands/camel-jbang-cmd-route-topology.adoc[route-topology] | Display inter-route topology connections -| xref:jbang-commands/camel-jbang-cmd-semantic-evaluate.adoc[semantic-evaluate] | Evaluate a semantic definition or expert operation | xref:jbang-commands/camel-jbang-cmd-send.adoc[send] | Send messages to endpoints | xref:jbang-commands/camel-jbang-cmd-span.adoc[span] | Display OpenTelemetry spans from running Camel integrations | xref:jbang-commands/camel-jbang-cmd-sql.adoc[sql] | Execute SQL query on a DataSource diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc index cbf10685bbbd..304f8d109ecd 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-commands.adoc @@ -41,6 +41,7 @@ TIP: You can also use `camel --help` or `camel <command> --help` to see availabl | xref:jbang-commands/camel-jbang-run.adoc[camel run] | Run as local Camel integration | xref:jbang-commands/camel-jbang-sbom.adoc[camel sbom] | Generate a CycloneDX or SPDX SBOM for a specific project | xref:jbang-commands/camel-jbang-script.adoc[camel script] | Run Camel integration as shell script for terminal scripting +| xref:jbang-commands/camel-jbang-semantic.adoc[camel semantic] | List semantic definitions and expert contracts | xref:jbang-commands/camel-jbang-shell.adoc[camel shell] | Interactive Camel CLI shell. | xref:jbang-commands/camel-jbang-stop.adoc[camel stop] | Shuts down running Camel integrations | xref:jbang-commands/camel-jbang-top.adoc[camel top] | Top status of Camel integrations diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get.adoc index d15d01264c16..1e4f3601dfc9 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get.adoc @@ -44,7 +44,6 @@ camel get [options] | xref:jbang-commands/camel-jbang-get-rest.adoc[rest] | Get REST services of Camel integrations | xref:jbang-commands/camel-jbang-get-route.adoc[route] | Get status of Camel routes | xref:jbang-commands/camel-jbang-get-route-controller.adoc[route-controller] | List status of route controller -| xref:jbang-commands/camel-jbang-get-semantic.adoc[semantic] | List semantic definitions and expert contracts | xref:jbang-commands/camel-jbang-get-service.adoc[service] | Get services of Camel integrations | xref:jbang-commands/camel-jbang-get-source.adoc[source] | Display Camel route source code | xref:jbang-commands/camel-jbang-get-sql-trace.adoc[sql-trace] | Get SQL query trace data diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-semantic-evaluate.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc similarity index 94% rename from docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-semantic-evaluate.adoc rename to docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc index bdb5e5089a3c..46a10dfd9c03 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-semantic-evaluate.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-eval.adoc @@ -1,6 +1,6 @@ // AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE -= camel cmd semantic-evaluate += camel semantic eval Evaluate a semantic definition or expert operation @@ -9,7 +9,7 @@ Evaluate a semantic definition or expert operation [source,bash] ---- -camel cmd semantic-evaluate [options] +camel semantic eval [options] ---- diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-semantic.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-get.adoc similarity index 91% copy from docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-semantic.adoc copy to docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-get.adoc index ba1b6d8db73e..78378a9f5756 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-semantic.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic-get.adoc @@ -1,6 +1,6 @@ // AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE -= camel get semantic += camel semantic get List semantic definitions and expert contracts @@ -9,7 +9,7 @@ List semantic definitions and expert contracts [source,bash] ---- -camel get semantic [options] +camel semantic get [options] ---- diff --git a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-semantic.adoc b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic.adoc similarity index 61% rename from docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-semantic.adoc rename to docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic.adoc index ba1b6d8db73e..ddfdae7e4972 100644 --- a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-get-semantic.adoc +++ b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-semantic.adoc @@ -1,6 +1,6 @@ // AUTO-GENERATED by camel-package-maven-plugin - DO NOT EDIT THIS FILE -= camel get semantic += camel semantic List semantic definitions and expert contracts @@ -9,10 +9,20 @@ List semantic definitions and expert contracts [source,bash] ---- -camel get semantic [options] +camel semantic [options] ---- +== Subcommands + +[cols="2,5",options="header"] +|=== +| Subcommand | Description +| xref:jbang-commands/camel-jbang-semantic-eval.adoc[eval] | Evaluate a semantic definition or expert operation +| xref:jbang-commands/camel-jbang-semantic-get.adoc[get] | List semantic definitions and expert contracts +|=== + + == Options diff --git a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json index 3b83afa8e364..b89a9e4721d0 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json +++ b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json @@ -3,7 +3,7 @@ { "name": "ask", "fullName": "ask", "description": "Ask a question about a running Camel application using AI", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": "--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama', 'openai', 'anthropic', or 'watsonx'", "javaType": [...] { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind source and sink Kamelets as a new Camel integration", "deprecated": true, "sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": [ { "names": "--error-handler", "description": "Add error handler (none|log|sink:<endpoint>). Sink endpoints are expected in the format [[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet name.", "javaType": "java.lang.String", "type": "stri [...] { "name": "catalog", "fullName": "catalog", "description": "List artifacts from Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "component", "fullName": "catalog component", "description": "List components from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...] - { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] + { "name": "cmd", "fullName": "cmd", "description": "Performs commands in the running Camel integrations, such as start\/stop route, or change logging levels.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "browse", "fullName": "cmd browse", "description": "Browse pending messages on endpoints [...] { "name": "completion", "fullName": "completion", "description": "Generate completion script for bash\/zsh", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "config", "fullName": "config", "description": "Get and set user configuration values", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "config get", "description": "Display user configuration value", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...] { "name": "debug", "fullName": "debug", "description": "Debug local Camel integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", "options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": "--background", "description": "Run in the background", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--background-wait", "description": "To [...] @@ -15,7 +15,7 @@ { "name": "eval", "fullName": "eval", "description": "Evaluate Camel expressions and scripts", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.EvalCommand", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "expression", "fullName": "eval expression", "description": "Evaluates Camel expression", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.EvalEx [...] { "name": "explain", "fullName": "explain", "description": "Explain what a Camel route does using AI\/LLM", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Explain", "options": [ { "names": "--api-key", "description": "API key for authentication. Also reads ANTHROPIC_API_KEY, OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama', 'openai' (OpenAI-compatible), ' [...] { "name": "export", "fullName": "export", "description": "Export to other runtimes (Camel Main, Spring Boot, or Quarkus)", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Export", "options": [ { "names": "--build-property", "description": "Maven build properties, ex. --build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, { "names": "--camel-spring-boot-version", "description": "Camel version to use with Spring Boot", "javaType": "java.lang.String", "ty [...] - { "name": "get", "fullName": "get", "description": "Get status of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "activity", "fullName": " [...] + { "name": "get", "fullName": "get", "description": "Get status of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.CamelStatus", "options": [ { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "activity", "fullName": " [...] { "name": "harden", "fullName": "harden", "description": "Suggest security hardening for Camel routes using AI\/LLM", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Harden", "options": [ { "names": "--api-key", "description": "API key for authentication. Also reads OPENAI_API_KEY or LLM_API_KEY env vars", "javaType": "java.lang.String", "type": "string" }, { "names": "--api-type", "description": "API type: 'ollama' or 'openai' (OpenAI-compatible)", "defaultValue": "ollama", [...] { "name": "hawtio", "fullName": "hawtio", "description": "Launch Hawtio web console", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.Hawtio", "options": [ { "names": "--host", "description": "Hostname to bind the Hawtio web console to", "defaultValue": "127.0.0.1", "javaType": "java.lang.String", "type": "string" }, { "names": "--openUrl", "description": "To automatic open Hawtio web console in the web browser", "defaultValue": "true", "javaType": "boolean", "type": [...] { "name": "infra", "fullName": "infra", "description": "List and Run external services for testing and prototyping", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.infra.InfraCommand", "options": [ { "names": "--json", "description": "Output in JSON Format", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", "fullName": "infra [...] @@ -30,6 +30,7 @@ { "name": "run", "fullName": "run", "description": "Run as local Camel integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Run", "options": [ { "names": "--background", "description": "Run in the background", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--background-wait", "description": "To wait for run in background to startup successfully, before returning", "defaultValue": "true", "javaType": "boolean", "type": "boolean" }, { [...] { "name": "sbom", "fullName": "sbom", "description": "Generate a CycloneDX or SPDX SBOM for a specific project", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.SBOMGenerator", "options": [ { "names": "--build-property", "description": "Maven build properties, ex. --build-property=prop1=foo", "javaType": "java.util.List", "type": "array" }, { "names": "--camel-spring-boot-version", "description": "Camel version to use with Spring Boot", "javaType": "java.lang.String", "type" [...] { "name": "script", "fullName": "script", "description": "Run Camel integration as shell script for terminal scripting", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Script", "options": [ { "names": "--logging", "description": "Can be used to turn on logging (logs to file in <user home>\/.camel directory)", "defaultValue": "false", "javaType": "boolean", "type": "boolean" }, { "names": "--logging-level", "description": "Logging level (ERROR, WARN, INFO, DEBUG, TRACE)", "d [...] + { "name": "semantic", "fullName": "semantic", "description": "List semantic definitions and expert contracts", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.action.CamelSemantic", "options": [ { "names": "--expert", "description": "Show the operations and parameter contract of this expert", "javaType": "java.lang.String", "type": "string" }, { "names": "--json", "description": "Output a single JSON document", "javaType": "boolean", "type": "boolean" }, { "names": "--timeou [...] { "name": "shell", "fullName": "shell", "description": "Interactive Camel CLI shell.", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Shell", "options": [ { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "stop", "fullName": "stop", "description": "Shuts down running Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.StopProcess", "options": [ { "names": "--kill", "description": "To force killing the process (SIGKILL)", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ] }, { "name": "top", "fullName": "top", "description": "Top status of Camel integrations", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.process.CamelTop", "options": [ { "names": "--watch", "description": "Execute periodically and showing output fullscreen", "javaType": "boolean", "type": "boolean" }, { "names": "-h,--help", "description": "Display the help and sub-commands", "javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "context", "fullName": "top [...] diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java index 6b2d2bee8eca..41b63bd7a972 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java @@ -123,7 +123,6 @@ public class CamelJBangMain implements Callable<Integer> { .addSubcommand("route-dump", new CommandLine(new CamelRouteDumpAction(this))) .addSubcommand("route-structure", new CommandLine(new CamelRouteStructureAction(this))) .addSubcommand("route-topology", new CommandLine(new CamelRouteTopologyAction(this))) - .addSubcommand("semantic-evaluate", new CommandLine(new SemanticEvaluate(this))) .addSubcommand("send", new CommandLine(new CamelSendAction(this))) .addSubcommand("sql", new CommandLine(new CamelSqlQueryAction(this))) .addSubcommand("span", new CommandLine(new CamelSpanAction(this))) @@ -184,7 +183,6 @@ public class CamelJBangMain implements Callable<Integer> { .addSubcommand("rest", new CommandLine(new ListRest(this))) .addSubcommand("route", new CommandLine(new CamelRouteStatus(this))) .addSubcommand("route-controller", new CommandLine(new RouteControllerAction(this))) - .addSubcommand("semantic", new CommandLine(new CamelSemantic(this))) .addSubcommand("service", new CommandLine(new ListService(this))) .addSubcommand("source", new CommandLine(new CamelSourceAction(this))) .addSubcommand("sql-trace", new CommandLine(new ListSqlTrace(this))) @@ -215,6 +213,9 @@ public class CamelJBangMain implements Callable<Integer> { .addSubcommand("run", new CommandLine(new Run(this))) .addSubcommand("sbom", new CommandLine(new SBOMGenerator(this))) .addSubcommand("script", new CommandLine(new Script(this))) + .addSubcommand("semantic", new CommandLine(new CamelSemantic(this)) + .addSubcommand("get", new CommandLine(new SemanticGet(this))) + .addSubcommand("eval", new CommandLine(new SemanticEvaluate(this)))) .addSubcommand("shell", new CommandLine(new Shell(this))) .addSubcommand("stop", new CommandLine(new StopProcess(this))) .addSubcommand("top", new CommandLine(new CamelTop(this)) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSemantic.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSemantic.java index b9c167ba7ee1..8f6a22246022 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSemantic.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelSemantic.java @@ -16,6 +16,12 @@ */ package org.apache.camel.dsl.jbang.core.commands.action; +import java.util.List; +import java.util.stream.IntStream; + +import com.github.freva.asciitable.AsciiTable; +import com.github.freva.asciitable.Column; +import com.github.freva.asciitable.HorizontalAlign; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; @@ -23,7 +29,12 @@ import org.apache.camel.util.json.Jsoner; import picocli.CommandLine; @CommandLine.Command(name = "semantic", description = "List semantic definitions and expert contracts", - sortOptions = false, showDefaultValues = true) + sortOptions = false, showDefaultValues = true, + footer = { + "%nExamples:", + " camel semantic my-app", + " camel semantic get my-app --expert=guard", + " camel semantic eval my-app --evaluation=safe --body='Sample message' --json" }) public class CamelSemantic extends SemanticActionCommand { @CommandLine.Option(names = "--expert", description = "Show the operations and parameter contract of this expert") @@ -62,34 +73,36 @@ public class CamelSemantic extends SemanticActionCommand { @Override protected void render(JsonObject response) { if (expert != null) { - printer().println("Expert: " + expert); - renderOperations(response.getCollection("operations")); + JsonArray operations = response.getCollection("operations"); + if (operations != null) { + printer().println("Expert: " + expert); + renderOperations(operations); + } return; } printer().println("Definitions:"); - printer().println("NAME\tEXPERT\tOPERATION\tSTATE\tRESULT TYPE\tERROR"); - JsonArray evaluations = response.getCollection("evaluations"); - if (evaluations != null) { - for (int i = 0; i < evaluations.size(); i++) { - JsonObject value = evaluations.getMap(i); - printer().println(String.join("\t", text(value, "name"), text(value, "expert"), text(value, "operation"), - text(value, "state"), text(value, "resultType"), text(value, "error"))); - } - } + printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows(response.getCollection("evaluations")), List.of( + new Column().header("NAME").dataAlign(HorizontalAlign.LEFT).with(r -> text(r, "name")), + new Column().header("EXPERT").dataAlign(HorizontalAlign.LEFT).with(r -> text(r, "expert")), + new Column().header("OPERATION").dataAlign(HorizontalAlign.LEFT).with(r -> text(r, "operation")), + new Column().header("STATE").dataAlign(HorizontalAlign.LEFT).with(r -> text(r, "state")), + new Column().header("RESULT TYPE").dataAlign(HorizontalAlign.LEFT).with(r -> text(r, "resultType")), + new Column().header("ERROR").dataAlign(HorizontalAlign.LEFT).with(r -> text(r, "error"))))); printer().println("Experts:"); - JsonArray experts = response.getCollection("experts"); - if (experts != null) { - for (int i = 0; i < experts.size(); i++) { - JsonObject value = experts.getMap(i); - printer().println(text(value, "reference") + "\t" + text(value, "name") + "\t" + text(value, "error")); - } - } + printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows(response.getCollection("experts")), List.of( + new Column().header("REFERENCE").dataAlign(HorizontalAlign.LEFT).with(r -> text(r, "reference")), + new Column().header("NAME").dataAlign(HorizontalAlign.LEFT).with(r -> text(r, "name")), + new Column().header("ERROR").dataAlign(HorizontalAlign.LEFT).with(r -> text(r, "error"))))); printer().println("Default expert: " + text(response, "defaultExpert")); if (response.get("defaultError") != null) { printer().println("Default expert error: " + response.get("defaultError")); } } + private static List<JsonObject> rows(JsonArray values) { + return values == null ? List.of() : IntStream.range(0, values.size()).<JsonObject> mapToObj(values::getMap).toList(); + } + private void renderOperations(JsonArray operations) { for (int i = 0; i < operations.size(); i++) { JsonObject operation = operations.getMap(i); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java index f142c1ec01eb..6f0f9dff6d3b 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticActionCommand.java @@ -16,19 +16,17 @@ */ package org.apache.camel.dsl.jbang.core.commands.action; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.List; -import java.util.UUID; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; -import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.apache.camel.dsl.jbang.core.commands.exceptionhandler.UsageErrorHandler; +import org.apache.camel.dsl.jbang.core.common.RuntimeHelper; +import org.apache.camel.dsl.jbang.core.common.VersionHelper; import org.apache.camel.util.json.JsonObject; import picocli.CommandLine; -public abstract class SemanticActionCommand extends ActionBaseCommand { +public abstract class SemanticActionCommand extends ActionBaseCommand implements UsageErrorHandler { @CommandLine.Parameters(description = "Name or pid of a running Camel integration", arity = "0..1") String name = "*"; @@ -66,7 +64,16 @@ public abstract class SemanticActionCommand extends ActionBaseCommand { if (pids.size() != 1) { return error(3, "Multiple Camel integrations match " + name + ": " + pids + ". Specify a pid.", null); } - JsonObject response = execute(pids.get(0), request); + long pid = pids.get(0); + JsonObject status = loadStatus(pid); + JsonObject context = status == null ? null : status.getMap("context"); + String version = context == null ? null : context.getString("version"); + if (version != null && version.matches("\\d+\\.\\d+(?:\\..*)?") && !VersionHelper.isGE(version, "4.23")) { + return error(3, "Semantic tooling requires Camel 4.23 or newer; integration " + pid + + " runs Camel " + version, + null); + } + JsonObject response = RuntimeHelper.executeAction(pid, request, timeout); if (response == null) { return error(4, "No reply from integration within " + timeout + " milliseconds", null); } @@ -74,7 +81,7 @@ public abstract class SemanticActionCommand extends ActionBaseCommand { return error(3, "Semantic tooling is unavailable; the integration needs a matching camel-semantic version", null); } - if ("failed".equals(response.getString("status")) || "error".equals(response.getString("status"))) { + if ("failed".equals(response.getString("status"))) { String message = response.getString("error"); if (message == null || message.isBlank()) { message = "Semantic action failed"; @@ -95,28 +102,11 @@ public abstract class SemanticActionCommand extends ActionBaseCommand { && ProcessHandle.of(pid).filter(ProcessHandle::isAlive).isPresent(); } - private JsonObject execute(long pid, JsonObject request) throws Exception { - String id = UUID.randomUUID().toString(); - Path directory = CommandLineHelper.getCamelDir(); - Path action = directory.resolve(pid + "-action-" + id + ".json"); - Path output = directory.resolve(pid + "-output-" + id + ".json"); - Path temporary = directory.resolve(pid + "-" + id + ".tmp"); - try { - Files.writeString(temporary, request.toJson()); - Files.move(temporary, action, StandardCopyOption.ATOMIC_MOVE); - return getJsonObject(output, timeout); - } finally { - // Removing an unfinished request asks the connector to cancel the evaluation. - Files.deleteIfExists(action); - Files.deleteIfExists(output); - Files.deleteIfExists(temporary); - } - } - - public int usageError(String message, String[] args) { + @Override + public Integer usageError(String message, String[] args) { json = Arrays.stream(args).takeWhile(arg -> !"--".equals(arg)) .anyMatch(arg -> "--json".equals(arg) || "--json=true".equals(arg)); - return error(2, message, null); + return json ? error(2, message, null) : null; } protected int error(int code, String message, JsonObject result) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticEvaluate.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticEvaluate.java index b58777a3849f..cee694a82653 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticEvaluate.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticEvaluate.java @@ -29,10 +29,15 @@ import org.apache.camel.util.json.JsonObject; import org.apache.camel.util.json.Jsoner; import picocli.CommandLine; [email protected](name = "semantic-evaluate", description = "Evaluate a semantic definition or expert operation", - sortOptions = false, showDefaultValues = true) [email protected](name = "eval", description = "Evaluate a semantic definition or expert operation", + sortOptions = false, showDefaultValues = true, + footer = { + "%nExamples:", + " camel semantic eval my-app --evaluation=safe --body='Sample message' --json", + " camel semantic eval my-app --expert=guard --operation=detect --input='Sample message'" }) public class SemanticEvaluate extends SemanticActionCommand { + // Jsoner accepts trailing commas; command input must use strict JSON parsing. private static final ObjectReader JSON_VALUE = new ObjectMapper().readerFor(Object.class) .with(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); @@ -98,6 +103,9 @@ public class SemanticEvaluate extends SemanticActionCommand { request.put("variables", sampleVariables); } } else { + if (expert == null && operation == null && input == null) { + throw new IllegalArgumentException("Choose --evaluation=<name>, or --expert with --operation and --input"); + } requireText(expert, "--expert"); requireText(operation, "--operation"); if (input == null) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticGet.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticGet.java new file mode 100644 index 000000000000..cc4212b27aef --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticGet.java @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.action; + +import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import picocli.CommandLine; + [email protected](name = "get", description = "List semantic definitions and expert contracts", + sortOptions = false, showDefaultValues = true, + footer = { + "%nExamples:", + " camel semantic get my-app --json", + " camel semantic get my-app --expert=guard" }) +public class SemanticGet extends CamelSemantic { + + public SemanticGet(CamelJBangMain main) { + super(main); + } +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/MissingPluginParameterExceptionHandler.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/MissingPluginParameterExceptionHandler.java index 2d16706bcde5..cd7c46624b7a 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/MissingPluginParameterExceptionHandler.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/MissingPluginParameterExceptionHandler.java @@ -19,7 +19,6 @@ package org.apache.camel.dsl.jbang.core.commands.exceptionhandler; import java.io.PrintWriter; import java.util.Optional; -import org.apache.camel.dsl.jbang.core.commands.action.SemanticActionCommand; import org.apache.camel.dsl.jbang.core.common.PluginHelper; import org.apache.camel.dsl.jbang.core.common.PluginType; import picocli.CommandLine; @@ -34,8 +33,11 @@ public class MissingPluginParameterExceptionHandler implements IParameterExcepti public int handleParseException(ParameterException ex, String[] args) throws Exception { CommandLine cmd = ex.getCommandLine(); PrintWriter err = cmd.getErr(); - if (cmd.getCommand() instanceof SemanticActionCommand semantic) { - return semantic.usageError(ex.getMessage(), args); + if (cmd.getCommand() instanceof UsageErrorHandler handler) { + Integer exitCode = handler.usageError(ex.getMessage(), args); + if (exitCode != null) { + return exitCode; + } } if (ex.getMessage().startsWith("Unmatched argument at index 0") && args.length > 0) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/UsageErrorHandler.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/UsageErrorHandler.java new file mode 100644 index 000000000000..9a3787b73735 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/exceptionhandler/UsageErrorHandler.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.exceptionhandler; + +/** Optional command-specific rendering of usage errors. */ +public interface UsageErrorHandler { + + /** + * @return the exit code when handled, or {@code null} to use the standard picocli diagnostics + */ + Integer usageError(String message, String[] args); +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java index a492e4e57d72..4ed9da5462d7 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelper.java @@ -17,12 +17,15 @@ package org.apache.camel.dsl.jbang.core.common; import java.io.File; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; import java.util.UUID; import java.util.function.Consumer; +import java.util.function.Function; import org.apache.camel.support.PatternHelper; import org.apache.camel.util.FileUtil; @@ -37,7 +40,7 @@ import org.apache.camel.util.json.Jsoner; * to {@code {pid}-action-{requestId}.json} and reading the response from {@code {pid}-output-{requestId}.json}. Each * request gets a unique ID so concurrent callers (CLI, MCP server, etc.) don't interfere with each other. * <p> - * This class is used by both the {@code camel ask} CLI command and the MCP server's {@code RuntimeService}. + * This class is used by CLI commands, the TUI and the MCP server's {@code RuntimeService}. * * @since 4.21 */ @@ -187,39 +190,74 @@ public final class RuntimeHelper { } public static String executeAction(long pid, String action, Consumer<JsonObject> configure, long timeoutMs) { - String requestId = UUID.randomUUID().toString().substring(0, 8); - Path camelDir = CommandLineHelper.getCamelDir(); - Path outputFile = camelDir.resolve(pid + "-output-" + requestId + ".json"); - PathUtils.deleteFile(outputFile); - - JsonObject root = new JsonObject(); - root.put("action", action); + JsonObject request = new JsonObject(); + request.put("action", action); if (configure != null) { - configure.accept(root); + configure.accept(request); + } + try { + String response = executeAction(pid, request, timeoutMs, Function.identity()); + if (response != null) { + return response; + } + } catch (IOException e) { + // Preserve the existing text API's diagnostic when the request cannot be exchanged. } + return "Timeout waiting for response from PID " + pid + " for action: " + action; + } - Path actionFile = camelDir.resolve(pid + "-action-" + requestId + ".json"); - PathUtils.writeTextSafely(root.toJson(), actionFile); + /** + * Executes a request and waits for a complete JSON object. Each request uses independent files, published + * atomically, and removing an unfinished request asks the connector to cancel it. + * + * @return the response, or {@code null} on timeout or interruption + * @throws IOException if publishing or cleaning up the request fails + */ + public static JsonObject executeAction(long pid, JsonObject request, long timeoutMs) throws IOException { + return executeAction(pid, request, timeoutMs, text -> Jsoner.deserialize(text, (JsonObject) null)); + } + private static <T> T executeAction(long pid, JsonObject request, long timeoutMs, Function<String, T> reader) + throws IOException { + String requestId = UUID.randomUUID().toString(); + Path directory = CommandLineHelper.getCamelDir(); + Path actionFile = directory.resolve(pid + "-action-" + requestId + ".json"); + Path outputFile = directory.resolve(pid + "-output-" + requestId + ".json"); + Path temporary = directory.resolve(pid + "-" + requestId + ".tmp"); try { + Files.writeString(temporary, request.toJson()); + Files.move(temporary, actionFile, StandardCopyOption.ATOMIC_MOVE); StopWatch watch = new StopWatch(); while (watch.taken() < timeoutMs) { try { - Thread.sleep(POLL_INTERVAL_MS); - if (Files.exists(outputFile) && outputFile.toFile().length() > 0) { - return Files.readString(outputFile); + Thread.sleep(Math.min(POLL_INTERVAL_MS, Math.max(1, timeoutMs - watch.taken()))); + if (Files.exists(outputFile)) { + String text = Files.readString(outputFile); + if (!text.isEmpty()) { + T response = reader.apply(text); + if (response != null) { + return response; + } + } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; - } catch (Exception e) { - // retry + } catch (IOException e) { + // The runtime writes output in place; retry until the response is complete. } } - return "Timeout waiting for response from PID " + pid + " for action: " + action; + return null; } finally { - PathUtils.deleteFile(outputFile); - PathUtils.deleteFile(actionFile); + try { + Files.deleteIfExists(actionFile); + } finally { + try { + Files.deleteIfExists(outputFile); + } finally { + Files.deleteIfExists(temporary); + } + } } } diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java index 2ee93a075b0a..b6329a9a869f 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/SemanticCommandTest.java @@ -27,8 +27,10 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import java.util.function.IntSupplier; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; import org.apache.camel.dsl.jbang.core.commands.exceptionhandler.MissingPluginParameterExceptionHandler; @@ -42,6 +44,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; import picocli.CommandLine; @@ -94,7 +97,10 @@ class SemanticCommandTest { })); assertEquals("semantic-metadata", request.get().get("action")); assertEquals(true, request.get().get("overview")); - assertTrue(printer.getOutput().contains("safe\tguard\tdetect\t${body}\tboolean\tdefinition error")); + assertTrue(printer.getOutput().contains("RESULT TYPE")); + assertTrue(printer.getOutput().lines().anyMatch(line -> line.trim().matches( + "safe +guard +detect +\\$\\{body} +boolean +definition error"))); + assertFalse(printer.getOutput().contains("\t")); assertTrue(printer.getOutput().contains("Safety guard")); assertTrue(printer.getOutput().contains("expert error")); assertTrue(printer.getOutput().contains("No default expert")); @@ -133,6 +139,33 @@ class SemanticCommandTest { assertError(3, "unknown"); } + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void failedExpertLookupRetainsFailureExitCode(boolean json) throws Exception { + assertEquals(1, respond(new CamelSemantic(main()), r -> jsonUnchecked( + "{\"status\":\"failed\",\"error\":\"metadata dispatch failed\"}"), + "--expert=guard", "--json=" + json)); + assertFalse(errors.toString().contains("NullPointerException")); + if (json) { + assertError(1, "metadata dispatch failed"); + } else { + assertEquals("metadata dispatch failed" + System.lineSeparator(), errors.toString()); + assertEquals("", printer.getOutput()); + } + } + + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void textParseErrorsKeepSuggestionsSynopsisAndHelp(boolean evaluate) throws Exception { + SemanticActionCommand action = evaluate ? new SemanticEvaluate(main()) : new CamelSemantic(main()); + assertEquals(2, command(action).execute("--exper=guard")); + assertTrue(errors.toString().contains("--expert")); + assertTrue(errors.toString().contains("Usage:")); + assertTrue(errors.toString().contains("--help'")); + assertEquals("", printer.getOutput()); + assertTrue(actionFiles().isEmpty()); + } + @Test void evaluatesNamedDefinitionWithSampleExchangeWithoutDoubleEscaping() throws Exception { String body = "A \"quote\"\nwith a backslash \\ and = sign"; @@ -219,7 +252,7 @@ class SemanticCommandTest { void textUsageErrorsGoOnlyToStderr() throws Exception { assertEquals(2, command(new SemanticEvaluate(main())).execute(Long.toString(pid))); assertEquals("", printer.getOutput()); - assertTrue(errors.toString().contains("--expert is required")); + assertTrue(errors.toString().contains("Choose --evaluation=<name>, or --expert with --operation and --input")); assertTrue(actionFiles().isEmpty()); } @@ -332,8 +365,12 @@ class SemanticCommandTest { CamelJBangMain main = new CamelJBangMain() { @Override public void postAddCommands(CommandLine command, String[] args) { - assertNotNull(command.getSubcommands().get("get").getSubcommands().get("semantic")); - assertNotNull(command.getSubcommands().get("cmd").getSubcommands().get("semantic-evaluate")); + CommandLine semantic = command.getSubcommands().get("semantic"); + assertNotNull(semantic); + assertNotNull(semantic.getSubcommands().get("get")); + assertNotNull(semantic.getSubcommands().get("eval")); + assertFalse(command.getSubcommands().get("get").getSubcommands().containsKey("semantic")); + assertFalse(command.getSubcommands().get("cmd").getSubcommands().containsKey("semantic-evaluate")); } @Override @@ -347,10 +384,94 @@ class SemanticCommandTest { } }; main.withPrinter(printer); - main.execute("cmd", "semantic-evaluate", "--bad-option", "--json"); + main.execute("semantic", "eval", "--bad-option", "--json"); assertError(2, "Unknown option"); } + @ParameterizedTest + @CsvSource({ "false, true", "false, false", "true, true", "true, false" }) + void jsonParseErrorsWorkBeforeAndAfterInvalidOption(boolean evaluate, boolean jsonFirst) throws Exception { + SemanticActionCommand action = evaluate ? new SemanticEvaluate(main()) : new CamelSemantic(main()); + String[] args = jsonFirst + ? new String[] { "--json", "--exper=guard" } + : new String[] { "--exper=guard", "--json" }; + assertEquals(2, command(action).execute(args)); + assertError(2, "Unknown option"); + assertFalse(errors.toString().contains("Usage:")); + assertTrue(actionFiles().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = { "--json=false", "--" }) + void textParseErrorsDoNotEnableJson(String option) throws Exception { + assertEquals(2, command(new SemanticEvaluate(main())).execute("--exper=guard", option, + "--".equals(option) ? "--json" : "--json=false")); + assertTrue(errors.toString().contains("Usage:")); + assertEquals("", printer.getOutput()); + assertTrue(actionFiles().isEmpty()); + } + + @ParameterizedTest + @CsvSource({ "3.22.0,false", "4.9.9,true", "4.21.0,false", "4.22.1,true", "4.22.0.redhat-00001,false" }) + void rejectsOlderRuntimesWithoutPublishingRequest(String version, boolean evaluate) throws Exception { + writeVersion(version); + SemanticActionCommand action = evaluate ? new SemanticEvaluate(main()) : new CamelSemantic(main()); + String[] args = evaluate + ? new String[] { Long.toString(pid), "--evaluation=safe", "--json" } + : new String[] { Long.toString(pid), "--json" }; + assertEquals(3, command(action).execute(args)); + assertError(3, "requires Camel 4.23 or newer"); + assertTrue(errors.toString().contains(version)); + assertTrue(actionFiles().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = { "4.23.0", "4.23.0-SNAPSHOT", "4.23.0.redhat-00001", "4.24.0", "5.0.0", "unknown" }) + void acceptsSupportedOrUnknownRuntimeVersions(String version) throws Exception { + writeVersion(version); + assertEquals(0, respond(new CamelSemantic(main()), r -> jsonUnchecked("{\"evaluations\":[]}"), "--json")); + } + + @ParameterizedTest + @ValueSource(strings = { "", "get", "eval" }) + void mainDispatchesSemanticGroupIncludingDefaultGet(String subcommand) throws Exception { + AtomicInteger exitCode = new AtomicInteger(-1); + CamelJBangMain main = new CamelJBangMain() { + @Override + public void preExecute(CommandLine command, String[] args) { + command.setErr(new PrintWriter(errors, true)); + } + + @Override + public void quit(int code) { + exitCode.set(code); + } + }; + main.withPrinter(printer); + List<String> args = new ArrayList<>(List.of("semantic")); + if (!subcommand.isEmpty()) { + args.add(subcommand); + } + args.add(Long.toString(pid)); + args.add("--json"); + if ("eval".equals(subcommand)) { + args.add("--evaluation=safe"); + } + assertEquals(0, respond(() -> { + main.execute(args.toArray(String[]::new)); + return exitCode.get(); + }, request -> { + assertEquals("eval".equals(subcommand) ? "semantic-evaluate" : "semantic-metadata", request.get("action")); + return jsonUnchecked("{\"status\":\"success\",\"evaluations\":[]}"); + })); + assertEquals("success", json(printer.getOutput()).get("status")); + } + + private void writeVersion(String version) throws Exception { + Files.writeString(CommandLineHelper.getCamelDir().resolve(pid + "-status.json"), + "{\"context\":{\"name\":\"semantic-test\",\"phase\":4,\"version\":\"" + version + "\"}}"); + } + private CamelJBangMain main() { return new CamelJBangMain().withPrinter(printer); } @@ -362,6 +483,12 @@ class SemanticCommandTest { private int respond(SemanticActionCommand command, Function<JsonObject, JsonObject> response, String... options) throws Exception { + List<String> args = new ArrayList<>(List.of(options)); + args.add(Long.toString(pid)); + return respond(() -> command(command).execute(args.toArray(String[]::new)), response); + } + + private int respond(IntSupplier call, Function<JsonObject, JsonObject> response) throws Exception { var responder = CompletableFuture.runAsync(() -> { try { await().atMost(5, TimeUnit.SECONDS).until(() -> !actionFiles().isEmpty()); @@ -372,9 +499,7 @@ class SemanticCommandTest { throw new RuntimeException(e); } }, executor); - List<String> args = new ArrayList<>(List.of(options)); - args.add(Long.toString(pid)); - int result = command(command).execute(args.toArray(String[]::new)); + int result = call.getAsInt(); responder.get(5, TimeUnit.SECONDS); assertTrue(actionFiles().isEmpty()); return result; @@ -395,7 +520,7 @@ class SemanticCommandTest { JsonObject result = jsonUnchecked(printer.getOutput()); assertEquals("error", result.get("status")); assertEquals(code, result.getInteger("code")); - assertTrue(result.getString("message").contains(message)); + assertTrue(result.getString("message").contains(message), result.toJson()); assertFalse(errors.toString().isBlank()); } diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelperTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelperTest.java new file mode 100644 index 000000000000..967152e936cd --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/RuntimeHelperTest.java @@ -0,0 +1,145 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.common; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.camel.util.json.JsonObject; +import org.apache.camel.util.json.Jsoner; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class RuntimeHelperTest { + + @TempDir + Path home; + private Path previousHome; + private ExecutorService executor; + + @BeforeEach + void setup() throws IOException { + previousHome = CommandLineHelper.getHomeDir(); + CommandLineHelper.useHomeDir(home.toString()); + Files.createDirectories(CommandLineHelper.getCamelDir()); + executor = Executors.newSingleThreadExecutor(); + } + + @AfterEach + void cleanup() throws InterruptedException { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + CommandLineHelper.useHomeDir(previousHome.toString()); + } + + @Test + void textApiPreservesRawResponseAndRequestConfiguration() throws Exception { + var result = CompletableFuture.supplyAsync(() -> RuntimeHelper.executeAction(123, "route", + request -> request.put("id", "route1"), 5000), executor); + Path action = requestFile(); + JsonObject request = (JsonObject) Jsoner.deserialize(Files.readString(action)); + assertEquals("route", request.get("action")); + assertEquals("route1", request.get("id")); + String raw = "raw response\n with whitespace\n"; + Files.writeString(outputFile(action), raw); + assertEquals(raw, result.get(5, TimeUnit.SECONDS)); + assertNoRequestFiles(); + } + + @Test + void textApiPreservesTimeoutDiagnostic() throws Exception { + assertEquals("Timeout waiting for response from PID 123 for action: route", + RuntimeHelper.executeAction(123, "route", null, 100)); + assertNoRequestFiles(); + } + + @Test + void jsonApiWaitsForCompleteResponse() throws Exception { + JsonObject request = new JsonObject(); + request.put("action", "semantic-metadata"); + var result = executor.submit(() -> RuntimeHelper.executeAction(123, request, 5000)); + Path action = requestFile(); + assertEquals(request, Jsoner.deserialize(Files.readString(action))); + Path output = outputFile(action); + Files.writeString(output, "{\"operations\":["); + await().during(250, TimeUnit.MILLISECONDS).atMost(1, TimeUnit.SECONDS).until(() -> !result.isDone()); + Files.writeString(output, "{\"operations\":[]}"); + assertTrue(result.get(5, TimeUnit.SECONDS).getCollection("operations").isEmpty()); + assertNoRequestFiles(); + } + + @Test + void interruptionCancelsRequestAndPreservesInterruptFlag() throws Exception { + AtomicReference<Thread> worker = new AtomicReference<>(); + var result = executor.submit(() -> { + worker.set(Thread.currentThread()); + JsonObject response = RuntimeHelper.executeAction(123, new JsonObject(), 5000); + assertTrue(Thread.currentThread().isInterrupted()); + return response; + }); + Path request = requestFile(); + worker.get().interrupt(); + assertNull(result.get(5, TimeUnit.SECONDS)); + assertFalse(Files.exists(request)); + assertNoRequestFiles(); + } + + @Test + void jsonApiPropagatesPublicationFailureWhileTextApiKeepsItsDiagnostic() { + CommandLineHelper.useHomeDir(home.resolve("missing").toString()); + assertThrows(IOException.class, () -> RuntimeHelper.executeAction(123, new JsonObject(), 100)); + assertEquals("Timeout waiting for response from PID 123 for action: route", + RuntimeHelper.executeAction(123, "route", null, 100)); + } + + private Path requestFile() throws IOException { + await().atMost(5, TimeUnit.SECONDS).until(() -> !requests().isEmpty()); + return requests().get(0); + } + + private List<Path> requests() throws IOException { + try (var files = Files.list(CommandLineHelper.getCamelDir())) { + return files.filter(p -> p.getFileName().toString().startsWith("123-action-")).toList(); + } + } + + private Path outputFile(Path action) { + return action.resolveSibling(action.getFileName().toString().replace("-action-", "-output-")); + } + + private void assertNoRequestFiles() throws IOException { + try (var files = Files.list(CommandLineHelper.getCamelDir())) { + assertEquals(0, files.count()); + } + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java index f97b5ee0df60..1a422ca3c3bb 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MonitorContext.java @@ -16,9 +16,9 @@ */ package org.apache.camel.dsl.jbang.core.commands.tui; +import java.io.IOException; import java.nio.file.Path; import java.util.List; -import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; @@ -32,6 +32,7 @@ import dev.tamboui.tui.TuiRunner; import org.apache.camel.catalog.CamelCatalog; import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; import org.apache.camel.dsl.jbang.core.common.PathUtils; +import org.apache.camel.dsl.jbang.core.common.RuntimeHelper; import org.apache.camel.util.json.JsonObject; /** @@ -232,17 +233,10 @@ class MonitorContext { /** Request-specific files keep a late response from being consumed by another action. */ JsonObject executeIndependentAction(String pid, JsonObject request, long timeoutMs) { - String requestId = UUID.randomUUID().toString(); - Path directory = CommandLineHelper.getCamelDir(); - Path actionFile = directory.resolve(pid + "-action-" + requestId + ".json"); - Path outputFile = directory.resolve(pid + "-output-" + requestId + ".json"); - PathUtils.writeTextSafely(request.toJson(), actionFile); try { - // The runtime writes output in place; keep polling until the JSON is complete. - return TuiHelper.pollJsonResponse(outputFile, timeoutMs); - } finally { - PathUtils.deleteFile(outputFile); - PathUtils.deleteFile(actionFile); + return RuntimeHelper.executeAction(Long.parseLong(pid), request, timeoutMs); + } catch (IOException e) { + return null; } }
