From 1b645c9bbeffe224b3dcb8e9c1c12a2b9abdfd85 Mon Sep 17 00:00:00 2001 From: Luigi De Masi Date: Tue, 29 Sep 2026 12:32:35 +0200 Subject: [PATCH 01/11] CAMEL-25138: Add semantic question declarations to Java and XML DSLs Register fluent Java and native XML declarations in the shared semantic question registry before routes initialize. Preserve validation, source ownership, reload behavior, and the existing YAML and provider contracts. Preserve declarations in Java/YAML model exports, generate XML schemas and catalog metadata, and document both declaration forms. Co-authored-by: Codex Signed-off-by: Luigi De Masi --- .../camel/catalog/docs/semantic-language.adoc | 71 +++- .../camel/catalog/models-app.properties | 2 + .../camel/catalog/models-app/beans.json | 17 +- .../camel/catalog/models-app/camel.json | 17 +- .../models-app/semanticDefinition.json | 17 + .../semanticQuestionDefinition.json | 25 ++ .../apache/camel/catalog/models/routes.json | 3 +- .../camel/catalog/schemas/camel-spring.xsd | 36 ++ .../camel/catalog/schemas/camel-xml-io.xsd | 36 ++ components/camel-ai/camel-semantic/pom.xml | 5 + .../src/main/docs/semantic-language.adoc | 71 +++- .../DefaultSemanticDefinitionConfigurer.java | 95 +++++ .../camel/semantic/SemanticQuestions.java | 10 +- .../org/apache/camel/semantic-configurer | 1 + .../semantic/SemanticDeclarationDslTest.java | 364 ++++++++++++++++++ .../org/apache/camel/model/app/beans.json | 17 +- .../org/apache/camel/model/app/camel.json | 17 +- .../camel/model/app/semanticDefinition.json | 17 + .../model/app/semanticQuestionDefinition.json | 25 ++ .../org/apache/camel/model/routes.json | 3 +- .../org/apache/camel/model/app/jaxb.index | 2 + .../apache/camel/builder/RouteBuilder.java | 18 + .../apache/camel/model/RoutesDefinition.java | 14 + .../camel/model/app/BeansDefinition.java | 13 + .../camel/model/app/SemanticDefinition.java | 73 ++++ .../app/SemanticDefinitionConfigurer.java | 26 ++ .../model/app/SemanticQuestionDefinition.java | 183 +++++++++ .../camel/java/out/JavaDslModelWriter.java | 2 + .../camel/java/LwModelToJavaDumper.java | 3 + .../java/out/JavaDslModelWriterSupport.java | 39 ++ .../camel/java/out/JavaDslCompileTest.java | 13 + .../org/apache/camel/xml/in/ModelParser.java | 24 ++ .../org/apache/camel/xml/out/ModelWriter.java | 20 + .../src/test/resources/beans-semantic.xml | 39 ++ .../src/test/resources/semantic-routes.xml | 43 +++ .../camel/yaml/out/YamlModelWriter.java | 2 + .../camel/yaml/LwModelToYAMLDumper.java | 3 + .../yaml/out/YamlModelWriterSupport.java | 40 ++ .../yaml/out/YamlWriterEdgeCasesTest.java | 33 ++ .../core/commands/mcp/TransformTools.java | 3 + .../core/commands/mcp/TransformToolsTest.java | 14 + .../dsl/xml/io/XmlRoutesBuilderLoader.java | 75 +++- .../xml/io/XmlRoutesBuilderLoaderTest.java | 20 + .../camel-yaml-dsl-deserializers/pom.xml | 2 + dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml | 5 + .../camel/dsl/yaml/SemanticQuestionTest.java | 58 +++ .../velocity/model-java-dsl-writer.vm | 3 +- .../resources/velocity/model-yaml-writer.vm | 3 +- 48 files changed, 1561 insertions(+), 61 deletions(-) create mode 100644 catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticDefinition.json create mode 100644 catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticQuestionDefinition.json create mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java create mode 100644 components/camel-ai/camel-semantic/src/main/resources/META-INF/services/org/apache/camel/semantic-configurer create mode 100644 components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java create mode 100644 core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticDefinition.json create mode 100644 core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticQuestionDefinition.json create mode 100644 core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java create mode 100644 core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinitionConfigurer.java create mode 100644 core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticQuestionDefinition.java create mode 100644 core/camel-xml-io/src/test/resources/beans-semantic.xml create mode 100644 core/camel-xml-io/src/test/resources/semantic-routes.xml 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 f8ec5e5b7e3d0..955e180b4208a 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 @@ -96,8 +96,75 @@ Reloading a resource replaces its complete set of questions, including removing no longer present. Development-mode route reload also removes definitions from deleted or renamed files before parsing replacements. This replacement does not make the surrounding route reload transactional. Existing expressions resolve the current definition on their next evaluation. -Loading declarations does not perform inference. Java applications can register immutable -`SemanticQuestion` definitions using `SemanticQuestions.get(context).replace(source, questions)`. +Loading declarations does not perform inference. + +=== Java declarations + +Use `semanticQuestions().question(name)` in a `RouteBuilder` to declare questions alongside routes: + +[source,java] +---- +@Override +public void configure() { + semanticQuestions().question("department") + .type("choice") + .state("${header.myState}") + .instructions("Which department should handle this message?") + .criterion("billing", "Invoices, payments, and refunds") + .criterion("technical", "Bugs, outages, and technical problems") + .criterion("other", "Everything else"); + + from("direct:tickets") + .setProperty("department").language("semantic", "ref:department") + .to("direct:dispatch"); +} +---- + +Declarations are installed after `configure()` completes and before the routes are initialized. +They can appear before or after the routes that reference them. Use `.type("boolean")` with +optional `.threshold(0.8)`, `.uncertainty(0.1)` and `.uncertaintyPolicy("non-match")` for boolean +questions. Use `.type("score")` and successive `.level("description")` calls for ordered score +levels. Java applications can also continue to register immutable `SemanticQuestion` definitions +using `SemanticQuestions.get(context).replace(source, questions)`. + +=== XML declarations + +With xref:others:java-xml-io-dsl.adoc[XML DSL], place a `semantic` element inside the `camel` or +`routes` document, alongside the routes: + +[source,xml] +---- + + + + Which department should handle this message? + + + + + + + + + ref:department + + + + +---- + +For boolean questions, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional question +attributes. For score questions, replace the named `criterion` elements with ordered +`level` elements, such as `RoutineUrgentCritical`. +XML declarations are loaded before route references are initialized, including declarations +after a route or in another resource loaded together. Reload replaces the source's questions; +removing its declarations or deleting the resource removes obsolete definitions. + +Both Java and XML declarations require `camel-semantic` and use the same context-wide registry, +validation, defaults and adapters as YAML. They do not require `camel-yaml-dsl`. Their questions +can also be selected together using `refs:name1,name2`, as described below. + +=== Selected state A question's optional `state` Simple expression overrides `camel.language.semantic.default-state`, whose default is `$\{body}`. Selectors are compiled diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app.properties b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app.properties index 57c9cb5a106f0..caf3b24324217 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app.properties +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app.properties @@ -4,4 +4,6 @@ constructor constructors properties property +semanticDefinition +semanticQuestionDefinition sslContextParameters diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/beans.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/beans.json index f62e00a438a60..eb9b74f87e8ef 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/beans.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/beans.json @@ -14,13 +14,14 @@ "properties": { "component-scan": { "index": 0, "kind": "element", "displayName": "Component-scan", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Component scanning that can auto-discover Camel route builders from the classpath." }, "bean": { "index": 1, "kind": "element", "displayName": "Bean", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "List of bean definitions to be registered in the Camel registry." }, - "sslContextParameters": { "index": 2, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, - "dataFormat": { "index": 3, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, - "restConfiguration": { "index": 4, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, - "rest": { "index": 5, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, - "routeConfiguration": { "index": 6, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, - "routeTemplate": { "index": 7, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, - "templatedRoute": { "index": 8, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, - "route": { "index": 9, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } + "semantic": { "index": 2, "kind": "element", "displayName": "Semantic", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.app.SemanticDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic question declarations." }, + "sslContextParameters": { "index": 3, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, + "dataFormat": { "index": 4, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, + "restConfiguration": { "index": 5, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, + "rest": { "index": 6, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, + "routeConfiguration": { "index": 7, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, + "routeTemplate": { "index": 8, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, + "templatedRoute": { "index": 9, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, + "route": { "index": 10, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/camel.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/camel.json index 1ac564378c757..94b3cebf509f3 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/camel.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/camel.json @@ -14,13 +14,14 @@ "properties": { "component-scan": { "index": 0, "kind": "element", "displayName": "Component-scan", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Component scanning that can auto-discover Camel route builders from the classpath." }, "bean": { "index": 1, "kind": "element", "displayName": "Bean", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "List of bean definitions to be registered in the Camel registry." }, - "sslContextParameters": { "index": 2, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, - "dataFormat": { "index": 3, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, - "restConfiguration": { "index": 4, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, - "rest": { "index": 5, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, - "routeConfiguration": { "index": 6, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, - "routeTemplate": { "index": 7, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, - "templatedRoute": { "index": 8, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, - "route": { "index": 9, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } + "semantic": { "index": 2, "kind": "element", "displayName": "Semantic", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.app.SemanticDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic question declarations." }, + "sslContextParameters": { "index": 3, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, + "dataFormat": { "index": 4, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, + "restConfiguration": { "index": 5, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, + "rest": { "index": 6, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, + "routeConfiguration": { "index": 7, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, + "routeTemplate": { "index": 8, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, + "templatedRoute": { "index": 9, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, + "route": { "index": 10, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticDefinition.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticDefinition.json new file mode 100644 index 0000000000000..e66c595edd178 --- /dev/null +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticDefinition.json @@ -0,0 +1,17 @@ +{ + "model": { + "kind": "model", + "name": "semanticDefinition", + "title": "Semantic Definition", + "description": "Named, provider-independent semantic question declarations.", + "deprecated": false, + "label": "configuration", + "javaType": "org.apache.camel.model.app.SemanticDefinition", + "abstract": false, + "input": false, + "output": false + }, + "properties": { + "question": { "index": 0, "kind": "element", "displayName": "Question", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic questions shared by routes in this Camel context." } + } +} diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticQuestionDefinition.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticQuestionDefinition.json new file mode 100644 index 0000000000000..12d4bbcbe2567 --- /dev/null +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticQuestionDefinition.json @@ -0,0 +1,25 @@ +{ + "model": { + "kind": "model", + "name": "semanticQuestionDefinition", + "title": "Semantic Question Definition", + "description": "A semantic question with the same fields and policies as a YAML declaration.", + "deprecated": false, + "label": "configuration", + "javaType": "org.apache.camel.model.app.SemanticQuestionDefinition", + "abstract": false, + "input": false, + "output": false + }, + "properties": { + "name": { "index": 0, "kind": "attribute", "displayName": "Name", "group": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The context-wide question name." }, + "type": { "index": 1, "kind": "attribute", "displayName": "Type", "group": "common", "required": true, "type": "enum", "javaType": "java.lang.String", "enum": [ "boolean", "choice", "score" ], "deprecated": false, "autowired": false, "secret": false, "description": "The question type." }, + "state": { "index": 2, "kind": "attribute", "displayName": "State", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The Simple expression selecting the message state." }, + "threshold": { "index": 3, "kind": "attribute", "displayName": "Threshold", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The boolean decision threshold." }, + "uncertainty": { "index": 4, "kind": "attribute", "displayName": "Uncertainty", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The boolean uncertainty band." }, + "uncertaintyPolicy": { "index": 5, "kind": "attribute", "displayName": "Uncertainty Policy", "group": "common", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "fail", "non-match" ], "deprecated": false, "autowired": false, "secret": false, "description": "The boolean uncertainty policy." }, + "instructions": { "index": 6, "kind": "element", "displayName": "Instructions", "group": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Instructions describing the judgment to make." }, + "criterion": { "index": 7, "kind": "element", "displayName": "Criterion", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Named choice criteria, or optional true\/false boolean criteria." }, + "level": { "index": 8, "kind": "element", "displayName": "Level", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Ordered descriptive levels for a score question." } + } +} diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/routes.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/routes.json index ec9c05fa2baf7..ce9cc06276050 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/routes.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/routes.json @@ -15,6 +15,7 @@ "id": { "index": 0, "kind": "attribute", "displayName": "Id", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The id of this node" }, "note": { "index": 1, "kind": "attribute", "displayName": "Note", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The note for this node" }, "description": { "index": 2, "kind": "attribute", "displayName": "Description", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The description for this node" }, - "routes": { "index": 3, "kind": "element", "displayName": "Routes", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "oneOf": [ "route" ], "deprecated": false, "autowired": false, "secret": false, "description": "Contains the Camel routes" } + "routes": { "index": 3, "kind": "element", "displayName": "Routes", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "oneOf": [ "route" ], "deprecated": false, "autowired": false, "secret": false, "description": "Contains the Camel routes" }, + "semantic": { "index": 4, "kind": "element", "displayName": "Semantic", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.app.SemanticDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic question declarations." } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd index ac1f0016ee7c6..6275aec276732 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd @@ -12899,10 +12899,37 @@ Description of the template parameter for documentation purposes. + + + + + + + + + + + + + + + + + + + + + + + + + @@ -14934,6 +14961,15 @@ Component scanning that can auto-discover Camel route builders from the classpat + + + + + + + diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd index c35e47ebc6bf5..4dcbfe2ffdd24 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd @@ -12042,10 +12042,37 @@ Description of the template parameter for documentation purposes. + + + + + + + + + + + + + + + + + + + + + + + + + @@ -14077,6 +14104,15 @@ Component scanning that can auto-discover Camel route builders from the classpat + + + + + + + diff --git a/components/camel-ai/camel-semantic/pom.xml b/components/camel-ai/camel-semantic/pom.xml index 7c3e05e0eafe7..833a7d9174d36 100644 --- a/components/camel-ai/camel-semantic/pom.xml +++ b/components/camel-ai/camel-semantic/pom.xml @@ -47,6 +47,11 @@ camel-test-junit6 test + + org.apache.camel + camel-xml-io-dsl + test + org.apache.camel camel-main 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 f8ec5e5b7e3d0..955e180b4208a 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 @@ -96,8 +96,75 @@ Reloading a resource replaces its complete set of questions, including removing no longer present. Development-mode route reload also removes definitions from deleted or renamed files before parsing replacements. This replacement does not make the surrounding route reload transactional. Existing expressions resolve the current definition on their next evaluation. -Loading declarations does not perform inference. Java applications can register immutable -`SemanticQuestion` definitions using `SemanticQuestions.get(context).replace(source, questions)`. +Loading declarations does not perform inference. + +=== Java declarations + +Use `semanticQuestions().question(name)` in a `RouteBuilder` to declare questions alongside routes: + +[source,java] +---- +@Override +public void configure() { + semanticQuestions().question("department") + .type("choice") + .state("${header.myState}") + .instructions("Which department should handle this message?") + .criterion("billing", "Invoices, payments, and refunds") + .criterion("technical", "Bugs, outages, and technical problems") + .criterion("other", "Everything else"); + + from("direct:tickets") + .setProperty("department").language("semantic", "ref:department") + .to("direct:dispatch"); +} +---- + +Declarations are installed after `configure()` completes and before the routes are initialized. +They can appear before or after the routes that reference them. Use `.type("boolean")` with +optional `.threshold(0.8)`, `.uncertainty(0.1)` and `.uncertaintyPolicy("non-match")` for boolean +questions. Use `.type("score")` and successive `.level("description")` calls for ordered score +levels. Java applications can also continue to register immutable `SemanticQuestion` definitions +using `SemanticQuestions.get(context).replace(source, questions)`. + +=== XML declarations + +With xref:others:java-xml-io-dsl.adoc[XML DSL], place a `semantic` element inside the `camel` or +`routes` document, alongside the routes: + +[source,xml] +---- + + + + Which department should handle this message? + + + + + + + + + ref:department + + + + +---- + +For boolean questions, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional question +attributes. For score questions, replace the named `criterion` elements with ordered +`level` elements, such as `RoutineUrgentCritical`. +XML declarations are loaded before route references are initialized, including declarations +after a route or in another resource loaded together. Reload replaces the source's questions; +removing its declarations or deleting the resource removes obsolete definitions. + +Both Java and XML declarations require `camel-semantic` and use the same context-wide registry, +validation, defaults and adapters as YAML. They do not require `camel-yaml-dsl`. Their questions +can also be selected together using `refs:name1,name2`, as described below. + +=== Selected state A question's optional `state` Simple expression overrides `camel.language.semantic.default-state`, whose default is `$\{body}`. Selectors are compiled diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java new file mode 100644 index 0000000000000..c6da03a7790ba --- /dev/null +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java @@ -0,0 +1,95 @@ +/* + * 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.semantic; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.model.PropertyDefinition; +import org.apache.camel.model.app.SemanticDefinition; +import org.apache.camel.model.app.SemanticDefinitionConfigurer; +import org.apache.camel.model.app.SemanticQuestionDefinition; +import org.apache.camel.spi.Resource; +import org.apache.camel.util.StringHelper; + +/** Converts Java/XML declarations to the same immutable questions used by YAML and programmatic registration. */ +public class DefaultSemanticDefinitionConfigurer implements SemanticDefinitionConfigurer { + @Override + public void configure(CamelContext context, Resource resource, String source, SemanticDefinition definition) { + Map questions = new LinkedHashMap<>(); + if (definition != null) { + for (SemanticQuestionDefinition question : definition.getQuestions()) { + String name = question.getName(); + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Semantic question requires a nonblank name"); + } + if (questions.containsKey(name)) { + throw new IllegalArgumentException("Duplicate semantic question: " + name); + } + try { + questions.put(name, question(question)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid semantic question '" + name + "': " + e.getMessage(), e); + } + } + } + if (resource != null) { + // A YAML loader can register its own declarations from the same resource before its RouteBuilder runs. + SemanticQuestions.get(context).replace("model:" + resource.getLocation(), resource, questions); + } else { + SemanticQuestions.get(context).replace(source, questions); + } + } + + private static SemanticQuestion question(SemanticQuestionDefinition definition) { + if (definition.getType() == null) { + throw new IllegalArgumentException("Question type is required"); + } + SemanticQuestion.Type type = enumeration(definition.getType(), SemanticQuestion.Type.class); + if (type != SemanticQuestion.Type.BOOLEAN + && (definition.getThreshold() != null || definition.getUncertainty() != null + || definition.getUncertaintyPolicy() != null)) { + throw new IllegalArgumentException("Threshold and uncertainty policy require a boolean question"); + } + Map criteria = new LinkedHashMap<>(); + for (PropertyDefinition criterion : definition.getCriteria()) { + if (criteria.containsKey(criterion.getKey())) { + throw new IllegalArgumentException("Duplicate semantic criterion: " + criterion.getKey()); + } + criteria.put(criterion.getKey(), criterion.getValue()); + } + return new SemanticQuestion( + type, definition.getInstructions(), definition.getState(), criteria, + definition.getLevels(), + definition.getThreshold() == null ? 0.5 : Double.parseDouble(definition.getThreshold()), + definition.getUncertainty() == null ? 0 : Double.parseDouble(definition.getUncertainty()), + definition.getUncertaintyPolicy() == null + ? SemanticQuestion.UncertaintyPolicy.FAIL + : enumeration(definition.getUncertaintyPolicy(), SemanticQuestion.UncertaintyPolicy.class)); + } + + private static > T enumeration(String value, Class type) { + String normalized = StringHelper.asEnumConstantValue(value); + for (T constant : type.getEnumConstants()) { + if (constant.name().equalsIgnoreCase(value) || constant.name().equalsIgnoreCase(normalized)) { + return constant; + } + } + throw new IllegalArgumentException("Invalid " + type.getSimpleName() + ": " + value); + } +} diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java index a326b3f766fb3..5597e348ad5c0 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java @@ -70,15 +70,19 @@ public synchronized void replace(String source, Map de /** Track a route resource so deleted files can be discarded before development-mode reload. */ public synchronized void replace(Resource source, Map definitions) { + replace(source.getLocation(), source, definitions); + } + + synchronized void replace(String location, Resource source, Map definitions) { removeDeletedResources(); - replace(source.getLocation(), definitions); + replace(location, definitions); if (!definitions.isEmpty() && "file".equals(source.getScheme())) { - resources.put(source.getLocation(), source); + resources.put(location, source); } } synchronized void removeDeletedResources() { - resources.values().stream().filter(resource -> !resource.exists()).map(Resource::getLocation).toList() + resources.entrySet().stream().filter(entry -> !entry.getValue().exists()).map(Map.Entry::getKey).toList() .forEach(location -> replace(location, Map.of())); } diff --git a/components/camel-ai/camel-semantic/src/main/resources/META-INF/services/org/apache/camel/semantic-configurer b/components/camel-ai/camel-semantic/src/main/resources/META-INF/services/org/apache/camel/semantic-configurer new file mode 100644 index 0000000000000..9d239b7c3eee0 --- /dev/null +++ b/components/camel-ai/camel-semantic/src/main/resources/META-INF/services/org/apache/camel/semantic-configurer @@ -0,0 +1 @@ +class=org.apache.camel.semantic.DefaultSemanticDefinitionConfigurer diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java new file mode 100644 index 0000000000000..79b3ca75e1b6c --- /dev/null +++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java @@ -0,0 +1,364 @@ +/* + * 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.semantic; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.apache.camel.Exchange; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.language.semantic.SemanticLanguage; +import org.apache.camel.model.app.SemanticDefinition; +import org.apache.camel.spi.Resource; +import org.apache.camel.support.DefaultExchange; +import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.ResourceHelper; +import org.apache.camel.support.RouteWatcherReloadStrategy; +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 org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class SemanticDeclarationDslTest { + @TempDir + Path directory; + private DefaultCamelContext context; + private final List states = new ArrayList<>(); + + @BeforeEach + void setup() throws Exception { + context = new DefaultCamelContext(); + context.getRegistry().bind("adapter", new SemanticAdapter() { + @Override + public void validate(SemanticQuestion question) { + } + + @Override + public SemanticResult evaluate(SemanticQuestion question, Object state) { + states.add(state); + return switch (question.getType()) { + case BOOLEAN -> new SemanticResult(null, 0.82, null, null, null); + case CHOICE -> new SemanticResult("billing", null, null, null, null); + case SCORE -> new SemanticResult(1.2, null, null, null, null); + }; + } + }); + ((SemanticLanguage) context.resolveLanguage("semantic")).setAdapter("adapter"); + context.start(); + } + + @AfterEach + void close() throws Exception { + context.close(); + } + + private static void questions(SemanticDefinition semantic) { + semantic.question("urgent").type("boolean").instructions("Is this urgent?") + .state("${header.myState}").threshold(0.8).uncertainty(0.1).uncertaintyPolicy("non-match"); + semantic.question("department").type("choice").instructions("Which department?") + .state("${header.myState}").criterion("billing", "Invoices and refunds") + .criterion("technical", "Bugs and outages"); + semantic.question("priority").type("score").instructions("How urgent?") + .state("${header.myState}").level("Routine").level("Urgent").level("Critical"); + } + + private static String xmlQuestions() { + return """ + + + Is this urgent? + + + Which department? + + + + + How urgent? + RoutineUrgentCritical + + + """; + } + + private static String document(String root, String contents) { + return "<" + root + " xmlns=\"http://camel.apache.org/schema/spring\">" + contents + ""; + } + + @ParameterizedTest + @ValueSource(strings = { "java", "camel", "routes" }) + void nativeDeclarationsEvaluateSingleQuestionsAndMixedBatches(String dsl) throws Exception { + if (dsl.equals("java")) { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("direct:batch").setProperty("decision").language("semantic", "refs:urgent,department,priority") + .setHeader("department", simple("${exchangeProperty.decision[department]}")); + // Declarations are collected before route initialization, including those after the route. + questions(semanticQuestions()); + } + }); + } else { + String route = """ + + + + refs:urgent,department,priority + + ${exchangeProperty.decision[department]} + + """; + load("questions.xml", document(dsl, route + xmlQuestions())); + } + assertThat(states).isEmpty(); + SemanticQuestion urgent = SemanticQuestions.get(context).get("urgent"); + assertThat(urgent.getThreshold()).isEqualTo(0.8); + assertThat(urgent.getUncertaintyPolicy()).isEqualTo(SemanticQuestion.UncertaintyPolicy.NON_MATCH); + assertThat(SemanticQuestions.get(context).get("priority").getLevels()).containsExactly("Routine", "Urgent", "Critical"); + try (var template = context.createProducerTemplate()) { + Exchange exchange = template.request("direct:batch", e -> { + e.getMessage().setBody("original"); + e.getMessage().setHeader("myState", "invoice"); + }); + assertThat(exchange.getException()).isNull(); + assertThat(exchange.getProperty("decision", Map.class)) + .containsEntry("urgent", false).containsEntry("department", "billing").containsEntry("priority", 1.2); + assertThat(exchange.getMessage().getHeader("department")).isEqualTo("billing"); + assertThat(exchange.getMessage().getBody()).isEqualTo("original"); + assertThat(exchange.getProperty(SemanticLanguage.RESULTS, Map.class)).hasSize(3); + for (String name : List.of("urgent", "department", "priority")) { + Object value + = context.resolveLanguage("semantic").createExpression("ref:" + name).evaluate(exchange, Object.class); + assertThat(value).isEqualTo(exchange.getProperty("decision", Map.class).get(name)); + } + assertThat(states).containsExactly("invoice", "invoice", "invoice", "invoice", "invoice", "invoice"); + } + } + + @ParameterizedTest + @MethodSource("invalidXml") + void invalidDeclarationsLeaveThePreviousDefinitionsIntact(String declaration, String message) throws Exception { + load("questions.xml", document("routes", xmlQuestions())); + SemanticQuestion previous = SemanticQuestions.get(context).get("department"); + assertThatThrownBy(() -> PluginHelper.getRoutesLoader(context).updateRoutes( + ResourceHelper.fromString("questions.xml", document("routes", "" + declaration + "")))) + .hasMessageContaining(message); + assertThat(SemanticQuestions.get(context).get("department")).isSameAs(previous); + assertThat(states).isEmpty(); + PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.xml", + document("routes", xmlQuestions().replace("header.myState", "header.corrected")))); + assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${header.corrected}"); + } + + static Stream invalidXml() { + String question = "Valid?"; + return Stream.of( + Arguments.of(question + question, "Duplicate semantic question"), + Arguments.of(question.replace("name=\"q\"", "name=\" \""), "nonblank name"), + Arguments.of(question.replace("type=\"boolean\"", ""), "type is required"), + Arguments.of(question.replace("boolean", "unknown"), "Invalid semantic question"), + Arguments.of(question.replace("Valid?", " "), "instructions must not be blank"), + Arguments.of(question.replace("type=\"boolean\"", "type=\"choice\" threshold=\"0.8\""), "require a boolean"), + Arguments.of( + question.replace("", + ""), + "Duplicate semantic criterion"), + Arguments.of(question.replace("type=\"boolean\"", "type=\"score\""), "score needs ordered levels"), + Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" threshold=\"NaN\""), "within [0,1]"), + Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" state=\" \""), + "state selector must not be blank"), + Arguments.of(question.replace("name=\"q\"", "unknown=\"q\""), "Unexpected attribute")); + } + + @Test + void javaResourceReloadReplacesAndRemovesDeclarations() throws Exception { + class Questions extends RouteBuilder { + private final String name; + + Questions(String name) { + this.name = name; + setResource(ResourceHelper.fromString("Questions.java", "")); + } + + @Override + public void configure() { + if (name != null) { + semanticQuestions().question(name).type("boolean").instructions("Valid?"); + } + } + } + context.addRoutes(new Questions("first")); + context.addRoutes(new Questions("second")); + assertThatThrownBy(() -> SemanticQuestions.get(context).get("first")).hasMessageContaining("Unknown"); + assertThat(SemanticQuestions.get(context).get("second")).isNotNull(); + context.addRoutes(new Questions(null)); + assertThatThrownBy(() -> SemanticQuestions.get(context).get("second")).hasMessageContaining("Unknown"); + assertThat(states).isEmpty(); + } + + @Test + void embeddedBuilderInstancesOwnSeparateDeclarations() throws Exception { + class Questions extends RouteBuilder { + private final String name; + + Questions(String name) { + this.name = name; + } + + @Override + public void configure() { + if (name != null) { + semanticQuestions().question(name).type("boolean").instructions("Valid?"); + } + } + } + context.addRoutes(new Questions("first")); + context.addRoutes(new Questions("second")); + context.addRoutes(new Questions(null)); + assertThat(SemanticQuestions.get(context).get(List.of("first", "second"))).hasSize(2); + assertThatThrownBy(() -> context.addRoutes(new Questions("first"))).hasMessageContaining("Duplicate semantic question"); + assertThat(states).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = { "non-match", "nonMatch", "NON_MATCH", "NonMatch" }) + void policySpellingsMatchYaml(String policy) throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + semanticQuestions().question("urgent").type("BOOLEAN").instructions("Urgent?").uncertaintyPolicy(policy); + } + }); + assertThat(SemanticQuestions.get(context).get("urgent").getUncertaintyPolicy()) + .isEqualTo(SemanticQuestion.UncertaintyPolicy.NON_MATCH); + } + + @Test + void duplicateNamesAcrossJavaAndXmlAreRejected() throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + questions(semanticQuestions()); + } + }); + assertThatThrownBy(() -> load("duplicate.xml", document("routes", xmlQuestions()))) + .hasMessageContaining("Duplicate semantic question"); + assertThat(states).isEmpty(); + } + + @Test + void failedResourceBatchDoesNotReuseEarlierCachedDeclarationsOnRetry() throws Exception { + String second = "" + + "Valid?"; + var loader = PluginHelper.getRoutesLoader(context); + assertThatThrownBy(() -> loader.loadRoutes(List.of( + ResourceHelper.fromString("first.xml", document("routes", xmlQuestions())), + ResourceHelper.fromString("second.xml", document("routes", second.replace("boolean", "unknown")))))) + .hasMessageContaining("Invalid semantic question"); + loader.loadRoutes(List.of( + ResourceHelper.fromString("first.xml", + document("routes", xmlQuestions().replace("header.myState", "header.new"))), + ResourceHelper.fromString("second.xml", document("routes", second)))); + assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${header.new}"); + assertThat(SemanticQuestions.get(context).get("second")).isNotNull(); + assertThat(states).isEmpty(); + } + + @Test + void declarationsFromAnotherResourceAreInstalledBeforeReferences() throws Exception { + PluginHelper.getRoutesLoader(context).loadRoutes(List.of( + ResourceHelper.fromString("use.xml", document("routes", """ + + ref:department + + """)), + ResourceHelper.fromString("definitions.xml", document("camel", xmlQuestions())))); + assertThat(states).isEmpty(); + try (var template = context.createProducerTemplate()) { + assertThat(template.requestBodyAndHeader("direct:use", "original", "myState", "invoice")).isEqualTo("billing"); + } + } + + @Test + void unknownReferenceFailsAtRouteInitialization() { + assertThatThrownBy(() -> load("unknown.xml", document("routes", """ + + ref:missing + + """))).hasRootCauseMessage("Unknown semantic question: missing"); + assertThat(states).isEmpty(); + } + + @Test + void reloadingReplacesAndRemovesDeclarationsUsedByExistingExpressions() throws Exception { + load("questions.xml", document("camel", xmlQuestions())); + var expression = context.resolveLanguage("semantic").createExpression("ref:department"); + var exchange = new DefaultExchange(context); + exchange.getMessage().setHeader("myState", "old"); + assertThat(expression.evaluate(exchange, String.class)).isEqualTo("billing"); + PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.xml", + document("camel", xmlQuestions().replace("header.myState", "header.updated")))); + exchange.getMessage().setHeader("updated", "new"); + assertThat(expression.evaluate(exchange, String.class)).isEqualTo("billing"); + assertThat(states).containsExactly("old", "new"); + PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.xml", document("camel", ""))); + assertThatThrownBy(() -> expression.evaluate(exchange, Object.class)).hasMessageContaining("Unknown semantic question"); + } + + @Test + void watcherRemovesDeletedAndRenamedDeclarationResources() throws Exception { + Path original = directory.resolve("questions.xml"); + Files.writeString(original, document("camel", xmlQuestions())); + Resource source = ResourceHelper.resolveResource(context, original.toUri().toString()); + PluginHelper.getRoutesLoader(context).loadRoutes(source); + TestWatcher watcher = new TestWatcher(); + watcher.setCamelContext(context); + Path renamed = Files.move(original, directory.resolve("renamed.xml")); + watcher.reload(source); + assertThatThrownBy(() -> SemanticQuestions.get(context).get("department")).hasMessageContaining("Unknown"); + Resource replacement = ResourceHelper.resolveResource(context, renamed.toUri().toString()); + watcher.reload(replacement); + assertThat(watcher.getLastError()).isNull(); + assertThat(SemanticQuestions.get(context).get("department")).isNotNull(); + Files.delete(renamed); + watcher.reload(replacement); + assertThatThrownBy(() -> SemanticQuestions.get(context).get("department")).hasMessageContaining("Unknown"); + } + + private void load(String location, String xml) throws Exception { + PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString(location, xml)); + } + + private static class TestWatcher extends RouteWatcherReloadStrategy { + void reload(Resource resource) { + onRouteReload(List.of(resource), false); + } + } +} diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/beans.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/beans.json index f62e00a438a60..eb9b74f87e8ef 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/beans.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/beans.json @@ -14,13 +14,14 @@ "properties": { "component-scan": { "index": 0, "kind": "element", "displayName": "Component-scan", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Component scanning that can auto-discover Camel route builders from the classpath." }, "bean": { "index": 1, "kind": "element", "displayName": "Bean", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "List of bean definitions to be registered in the Camel registry." }, - "sslContextParameters": { "index": 2, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, - "dataFormat": { "index": 3, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, - "restConfiguration": { "index": 4, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, - "rest": { "index": 5, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, - "routeConfiguration": { "index": 6, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, - "routeTemplate": { "index": 7, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, - "templatedRoute": { "index": 8, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, - "route": { "index": 9, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } + "semantic": { "index": 2, "kind": "element", "displayName": "Semantic", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.app.SemanticDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic question declarations." }, + "sslContextParameters": { "index": 3, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, + "dataFormat": { "index": 4, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, + "restConfiguration": { "index": 5, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, + "rest": { "index": 6, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, + "routeConfiguration": { "index": 7, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, + "routeTemplate": { "index": 8, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, + "templatedRoute": { "index": 9, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, + "route": { "index": 10, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } } } diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/camel.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/camel.json index 1ac564378c757..94b3cebf509f3 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/camel.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/camel.json @@ -14,13 +14,14 @@ "properties": { "component-scan": { "index": 0, "kind": "element", "displayName": "Component-scan", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Component scanning that can auto-discover Camel route builders from the classpath." }, "bean": { "index": 1, "kind": "element", "displayName": "Bean", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "List of bean definitions to be registered in the Camel registry." }, - "sslContextParameters": { "index": 2, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, - "dataFormat": { "index": 3, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, - "restConfiguration": { "index": 4, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, - "rest": { "index": 5, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, - "routeConfiguration": { "index": 6, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, - "routeTemplate": { "index": 7, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, - "templatedRoute": { "index": 8, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, - "route": { "index": 9, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } + "semantic": { "index": 2, "kind": "element", "displayName": "Semantic", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.app.SemanticDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic question declarations." }, + "sslContextParameters": { "index": 3, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, + "dataFormat": { "index": 4, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, + "restConfiguration": { "index": 5, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, + "rest": { "index": 6, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, + "routeConfiguration": { "index": 7, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, + "routeTemplate": { "index": 8, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, + "templatedRoute": { "index": 9, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, + "route": { "index": 10, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } } } diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticDefinition.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticDefinition.json new file mode 100644 index 0000000000000..e66c595edd178 --- /dev/null +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticDefinition.json @@ -0,0 +1,17 @@ +{ + "model": { + "kind": "model", + "name": "semanticDefinition", + "title": "Semantic Definition", + "description": "Named, provider-independent semantic question declarations.", + "deprecated": false, + "label": "configuration", + "javaType": "org.apache.camel.model.app.SemanticDefinition", + "abstract": false, + "input": false, + "output": false + }, + "properties": { + "question": { "index": 0, "kind": "element", "displayName": "Question", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic questions shared by routes in this Camel context." } + } +} diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticQuestionDefinition.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticQuestionDefinition.json new file mode 100644 index 0000000000000..12d4bbcbe2567 --- /dev/null +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticQuestionDefinition.json @@ -0,0 +1,25 @@ +{ + "model": { + "kind": "model", + "name": "semanticQuestionDefinition", + "title": "Semantic Question Definition", + "description": "A semantic question with the same fields and policies as a YAML declaration.", + "deprecated": false, + "label": "configuration", + "javaType": "org.apache.camel.model.app.SemanticQuestionDefinition", + "abstract": false, + "input": false, + "output": false + }, + "properties": { + "name": { "index": 0, "kind": "attribute", "displayName": "Name", "group": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The context-wide question name." }, + "type": { "index": 1, "kind": "attribute", "displayName": "Type", "group": "common", "required": true, "type": "enum", "javaType": "java.lang.String", "enum": [ "boolean", "choice", "score" ], "deprecated": false, "autowired": false, "secret": false, "description": "The question type." }, + "state": { "index": 2, "kind": "attribute", "displayName": "State", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The Simple expression selecting the message state." }, + "threshold": { "index": 3, "kind": "attribute", "displayName": "Threshold", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The boolean decision threshold." }, + "uncertainty": { "index": 4, "kind": "attribute", "displayName": "Uncertainty", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The boolean uncertainty band." }, + "uncertaintyPolicy": { "index": 5, "kind": "attribute", "displayName": "Uncertainty Policy", "group": "common", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "fail", "non-match" ], "deprecated": false, "autowired": false, "secret": false, "description": "The boolean uncertainty policy." }, + "instructions": { "index": 6, "kind": "element", "displayName": "Instructions", "group": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Instructions describing the judgment to make." }, + "criterion": { "index": 7, "kind": "element", "displayName": "Criterion", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Named choice criteria, or optional true\/false boolean criteria." }, + "level": { "index": 8, "kind": "element", "displayName": "Level", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Ordered descriptive levels for a score question." } + } +} diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/routes.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/routes.json index ec9c05fa2baf7..ce9cc06276050 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/routes.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/routes.json @@ -15,6 +15,7 @@ "id": { "index": 0, "kind": "attribute", "displayName": "Id", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The id of this node" }, "note": { "index": 1, "kind": "attribute", "displayName": "Note", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The note for this node" }, "description": { "index": 2, "kind": "attribute", "displayName": "Description", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The description for this node" }, - "routes": { "index": 3, "kind": "element", "displayName": "Routes", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "oneOf": [ "route" ], "deprecated": false, "autowired": false, "secret": false, "description": "Contains the Camel routes" } + "routes": { "index": 3, "kind": "element", "displayName": "Routes", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "oneOf": [ "route" ], "deprecated": false, "autowired": false, "secret": false, "description": "Contains the Camel routes" }, + "semantic": { "index": 4, "kind": "element", "displayName": "Semantic", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.app.SemanticDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic question declarations." } } } diff --git a/core/camel-core-model/src/generated/resources/org/apache/camel/model/app/jaxb.index b/core/camel-core-model/src/generated/resources/org/apache/camel/model/app/jaxb.index index 0c1c62edebf95..55c247965cd5f 100644 --- a/core/camel-core-model/src/generated/resources/org/apache/camel/model/app/jaxb.index +++ b/core/camel-core-model/src/generated/resources/org/apache/camel/model/app/jaxb.index @@ -7,3 +7,5 @@ BeanPropertyDefinition BeansDefinition ComponentScanDefinition SSLContextParametersDefinition +SemanticDefinition +SemanticQuestionDefinition diff --git a/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java b/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java index 7110c7642a6f1..5897665f64f18 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java @@ -48,6 +48,7 @@ import org.apache.camel.model.RoutesDefinition; import org.apache.camel.model.TemplatedRouteDefinition; import org.apache.camel.model.TemplatedRoutesDefinition; +import org.apache.camel.model.app.SemanticDefinition; import org.apache.camel.model.errorhandler.RefErrorHandlerDefinition; import org.apache.camel.model.rest.RestConfigurationDefinition; import org.apache.camel.model.rest.RestDefinition; @@ -199,6 +200,16 @@ public String toString() { */ public abstract void configure() throws Exception; + /** + * Declares named semantic questions for use with the semantic language. Requires camel-semantic at runtime. + */ + public SemanticDefinition semanticQuestions() { + if (getRouteCollection().getSemantic() == null) { + getRouteCollection().setSemantic(new SemanticDefinition()); + } + return getRouteCollection().getSemantic(); + } + /** * Called on initialization to build routes configuration (global routes configurations) using the fluent builder * syntax. @@ -910,6 +921,13 @@ protected void checkInitialized() throws Exception { configure(); + if (getResource() != null || getRouteCollection().getSemantic() != null) { + String source = getResource() != null + ? getResource().getLocation() + : "java:" + camelContext.getUuidGenerator().generateUuid(); + SemanticDefinition.configure(camelContext, getResource(), source, getRouteCollection().getSemantic()); + } + // remember the source resource getRouteCollection().setResource(getResource()); getRestCollection().setResource(getResource()); diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java index d8bb7ee32ae07..9ca2b1b5b6305 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java @@ -23,6 +23,7 @@ import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlElementRef; import jakarta.xml.bind.annotation.XmlRootElement; import jakarta.xml.bind.annotation.XmlTransient; @@ -31,6 +32,7 @@ import org.apache.camel.Endpoint; import org.apache.camel.ErrorHandlerFactory; import org.apache.camel.builder.EndpointConsumerBuilder; +import org.apache.camel.model.app.SemanticDefinition; import org.apache.camel.spi.AsEndpointUri; import org.apache.camel.spi.Metadata; import org.apache.camel.spi.Resource; @@ -75,6 +77,18 @@ public class RoutesDefinition extends OptionalIdentifiedDefinition routes = new ArrayList<>(); + @XmlElement(name = "semantic") + @Metadata(description = "Named semantic question declarations.") + private SemanticDefinition semantic; + + public SemanticDefinition getSemantic() { + return semantic; + } + + public void setSemantic(SemanticDefinition semantic) { + this.semantic = semantic; + } + public RoutesDefinition() { } diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/BeansDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/app/BeansDefinition.java index 6643d11f6eb34..6065b1d1fdb6c 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/app/BeansDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/app/BeansDefinition.java @@ -54,6 +54,7 @@ @XmlType(propOrder = { "componentScanning", "beans", + "semantic", "springOrBlueprintBeans", "sslContextParameters", "dataFormats", @@ -79,6 +80,18 @@ public class BeansDefinition { @Metadata(description = "List of bean definitions to be registered in the Camel registry.") private List beans = new ArrayList<>(); + @XmlElement(name = "semantic") + @Metadata(description = "Named semantic question declarations.") + private SemanticDefinition semantic; + + public SemanticDefinition getSemantic() { + return semantic; + } + + public void setSemantic(SemanticDefinition semantic) { + this.semantic = semantic; + } + // support for legacy spring and blueprint files to be parsed and loaded // for migration and tooling effort (need to be in a single @XmlAnyElement as otherwise // this causes camel-spring-xml to generate an invalid XSD diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java new file mode 100644 index 0000000000000..66ae92ca24961 --- /dev/null +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java @@ -0,0 +1,73 @@ +/* + * 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.model.app; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlType; + +import org.apache.camel.CamelContext; +import org.apache.camel.spi.Metadata; +import org.apache.camel.spi.Resource; + +/** Named, provider-independent semantic question declarations. */ +@Metadata(label = "configuration") +@XmlType(name = "semanticDefinition") +@XmlAccessorType(XmlAccessType.FIELD) +public class SemanticDefinition { + @XmlElement(name = "question") + @Metadata(description = "Named semantic questions shared by routes in this Camel context.") + private List questions = new ArrayList<>(); + + public List getQuestions() { + return questions; + } + + public void setQuestions(List questions) { + this.questions = questions; + } + + /** Declare a named semantic question. */ + public SemanticQuestionDefinition question(String name) { + SemanticQuestionDefinition question = new SemanticQuestionDefinition(); + question.setName(name); + questions.add(question); + return question; + } + + /** Register declarations before route initialization, without requiring semantic support for ordinary routes. */ + public static void configure( + CamelContext context, Resource resource, String source, SemanticDefinition definition) { + SemanticDefinitionConfigurer configurer + = context.getCamelContextExtension().getContextPlugin(SemanticDefinitionConfigurer.class); + if (configurer == null) { + if (definition == null || definition.getQuestions().isEmpty()) { + return; + } + configurer = context.getCamelContextExtension().getDefaultFactoryFinder() + .newInstance("semantic-configurer", SemanticDefinitionConfigurer.class) + .orElseThrow(() -> new IllegalArgumentException( + "Semantic question declarations require camel-semantic on the classpath")); + context.getCamelContextExtension().addContextPlugin(SemanticDefinitionConfigurer.class, configurer); + } + configurer.configure(context, resource, source, definition); + } +} diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinitionConfigurer.java b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinitionConfigurer.java new file mode 100644 index 0000000000000..141ba19a93236 --- /dev/null +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinitionConfigurer.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.model.app; + +import org.apache.camel.CamelContext; +import org.apache.camel.spi.Resource; + +/** Installs semantic declarations using the optional semantic language module. */ +public interface SemanticDefinitionConfigurer { + /** Replace the questions belonging to this source, including removing obsolete declarations. */ + void configure(CamelContext context, Resource resource, String source, SemanticDefinition definition); +} diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticQuestionDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticQuestionDefinition.java new file mode 100644 index 0000000000000..1a5c70c833c38 --- /dev/null +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticQuestionDefinition.java @@ -0,0 +1,183 @@ +/* + * 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.model.app; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlAttribute; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlType; + +import org.apache.camel.model.PropertyDefinition; +import org.apache.camel.spi.Metadata; + +/** A semantic question with the same fields and policies as a YAML declaration. */ +@Metadata(label = "configuration") +@XmlType(name = "semanticQuestionDefinition", propOrder = { "instructions", "criteria", "levels" }) +@XmlAccessorType(XmlAccessType.FIELD) +public class SemanticQuestionDefinition { + @XmlAttribute(required = true) + @Metadata(required = true, description = "The context-wide question name.") + private String name; + @XmlAttribute(required = true) + @Metadata(required = true, enums = "boolean,choice,score", description = "The question type.") + private String type; + @XmlAttribute + @Metadata(description = "The Simple expression selecting the message state.") + private String state; + @XmlAttribute + @Metadata(description = "The boolean decision threshold.") + private String threshold; + @XmlAttribute + @Metadata(description = "The boolean uncertainty band.") + private String uncertainty; + @XmlAttribute + @Metadata(enums = "fail,non-match", description = "The boolean uncertainty policy.") + private String uncertaintyPolicy; + @XmlElement(required = true) + @Metadata(required = true, description = "Instructions describing the judgment to make.") + private String instructions; + @XmlElement(name = "criterion") + @Metadata(description = "Named choice criteria, or optional true/false boolean criteria.") + private List criteria = new ArrayList<>(); + @XmlElement(name = "level") + @Metadata(description = "Ordered descriptive levels for a score question.") + private List levels = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getThreshold() { + return threshold; + } + + public void setThreshold(String threshold) { + this.threshold = threshold; + } + + public String getUncertainty() { + return uncertainty; + } + + public void setUncertainty(String uncertainty) { + this.uncertainty = uncertainty; + } + + public String getUncertaintyPolicy() { + return uncertaintyPolicy; + } + + public void setUncertaintyPolicy(String uncertaintyPolicy) { + this.uncertaintyPolicy = uncertaintyPolicy; + } + + public String getInstructions() { + return instructions; + } + + public void setInstructions(String instructions) { + this.instructions = instructions; + } + + public List getCriteria() { + return criteria; + } + + public void setCriteria(List criteria) { + this.criteria = criteria; + } + + public List getLevels() { + return levels; + } + + public void setLevels(List levels) { + this.levels = levels; + } + + /** The question type. */ + public SemanticQuestionDefinition type(String type) { + this.type = type; + return this; + } + + /** The Simple expression selecting the message state. */ + public SemanticQuestionDefinition state(String state) { + this.state = state; + return this; + } + + /** The boolean decision threshold. */ + public SemanticQuestionDefinition threshold(double threshold) { + this.threshold = Double.toString(threshold); + return this; + } + + /** The boolean uncertainty band. */ + public SemanticQuestionDefinition uncertainty(double uncertainty) { + this.uncertainty = Double.toString(uncertainty); + return this; + } + + /** The boolean uncertainty policy. */ + public SemanticQuestionDefinition uncertaintyPolicy(String uncertaintyPolicy) { + this.uncertaintyPolicy = uncertaintyPolicy; + return this; + } + + /** Instructions describing the judgment to make. */ + public SemanticQuestionDefinition instructions(String instructions) { + this.instructions = instructions; + return this; + } + + /** Add a named criterion. */ + public SemanticQuestionDefinition criterion(String name, String description) { + criteria.add(new PropertyDefinition(name, description)); + return this; + } + + /** Add the next descriptive score level. */ + public SemanticQuestionDefinition level(String description) { + levels.add(description); + return this; + } +} diff --git a/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java b/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java index 6bd836608b24d..42843438f19a6 100644 --- a/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java +++ b/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java @@ -2474,6 +2474,7 @@ protected void doWriteRouteTemplatesDefinition(StringBuilder sb, RouteTemplatesD protected void doWriteRoutesDefinition(StringBuilder sb, RoutesDefinition def) { doWriteOptionalIdentifiedDefinitionAttributes(sb, def); doWriteChildList(sb, "routes", def.getRoutes(), this::doWriteRouteDefinitionRef); + doWriteChildElement(sb, "semantic", def.getSemantic(), this::doWriteSemanticDefinition); } protected void doWriteRoutingSlipDefinition(StringBuilder sb, RoutingSlipDefinition def) { doWriteProcessorDefinitionAttributes(sb, def); @@ -2788,6 +2789,7 @@ protected void doWriteBeanPropertyDefinition(StringBuilder sb, BeanPropertyDefin protected void doWriteBeansDefinitionElements(StringBuilder sb, BeansDefinition def) { doWriteChildList(sb, "component-scan", def.getComponentScanning(), this::doWriteComponentScanDefinition); doWriteChildList(sb, "bean", def.getBeans(), this::doWriteBeanFactoryDefinition); + doWriteChildElement(sb, "semantic", def.getSemantic(), this::doWriteSemanticDefinition); // @XmlAnyElement - not applicable for Java DSL doWriteChildList(sb, "sslContextParameters", def.getSslContextParameters(), this::doWriteSSLContextParametersDefinition); doWriteChildList(sb, "dataFormat", def.getDataFormats(), this::doWriteDataFormatDefinition); diff --git a/core/camel-java-io/src/main/java/org/apache/camel/java/LwModelToJavaDumper.java b/core/camel-java-io/src/main/java/org/apache/camel/java/LwModelToJavaDumper.java index a31ffe45207eb..b13b7283d7d9a 100644 --- a/core/camel-java-io/src/main/java/org/apache/camel/java/LwModelToJavaDumper.java +++ b/core/camel-java-io/src/main/java/org/apache/camel/java/LwModelToJavaDumper.java @@ -94,6 +94,9 @@ public String dumpModelAsJava( StringBuilder sb = new StringBuilder(); try { if (definition instanceof RoutesDefinition rd) { + if (rd.getSemantic() != null) { + sb.append(writer.writeSemanticDefinition(rd.getSemantic())); + } for (RouteDefinition route : rd.getRoutes()) { if (!sb.isEmpty()) { sb.append("\n\n"); diff --git a/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java b/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java index f54e1dcd2aa5b..67d18af71fc0b 100644 --- a/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java +++ b/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java @@ -43,6 +43,8 @@ import org.apache.camel.model.TemplatedRouteParameterDefinition; import org.apache.camel.model.ToDefinition; import org.apache.camel.model.ValueDefinition; +import org.apache.camel.model.app.SemanticDefinition; +import org.apache.camel.model.app.SemanticQuestionDefinition; import org.apache.camel.model.config.BatchResequencerConfig; import org.apache.camel.model.errorhandler.DeadLetterChannelDefinition; import org.apache.camel.model.errorhandler.DefaultErrorHandlerDefinition; @@ -1131,6 +1133,43 @@ protected String indent() { return " ".repeat(indentLevel); } + public String writeSemanticDefinition(SemanticDefinition definition) { + StringBuilder result = new StringBuilder(); + doWriteSemanticDefinition(result, definition); + return result.toString(); + } + + protected void doWriteSemanticDefinition(StringBuilder result, SemanticDefinition definition) { + for (SemanticQuestionDefinition question : definition.getQuestions()) { + if (!result.isEmpty()) { + result.append(NL); + } + result.append("semanticQuestions().question(").append(quote(question.getName())).append(")") + .append(".type(").append(quote(question.getType())).append(")") + .append(".instructions(").append(quote(question.getInstructions())).append(")"); + if (question.getState() != null) { + result.append(".state(").append(quote(question.getState())).append(")"); + } + if (question.getThreshold() != null) { + result.append(".threshold(").append(Double.parseDouble(question.getThreshold())).append(")"); + } + if (question.getUncertainty() != null) { + result.append(".uncertainty(").append(Double.parseDouble(question.getUncertainty())).append(")"); + } + if (question.getUncertaintyPolicy() != null) { + result.append(".uncertaintyPolicy(").append(quote(question.getUncertaintyPolicy())).append(")"); + } + for (PropertyDefinition criterion : question.getCriteria()) { + result.append(".criterion(").append(quote(criterion.getKey())).append(", ") + .append(quote(criterion.getValue())).append(")"); + } + for (String level : question.getLevels()) { + result.append(".level(").append(quote(level)).append(")"); + } + result.append(";"); + } + } + protected String quote(String s) { if (s == null) { return "null"; diff --git a/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java b/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java index edb798a0291f9..a8a462e986bb7 100644 --- a/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java +++ b/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java @@ -38,6 +38,7 @@ import javax.tools.SimpleJavaFileObject; import javax.tools.ToolProvider; +import org.apache.camel.java.LwModelToJavaDumper; import org.apache.camel.model.RouteConfigurationDefinition; import org.apache.camel.model.RouteConfigurationsDefinition; import org.apache.camel.model.RouteDefinition; @@ -132,6 +133,18 @@ private static void collectXmlRouteFiles(Path dir, List result) throws E } } + @Test + void semanticDeclarationsCompileWithTheirRoutes() throws Exception { + try (var input = Files.newInputStream(XML_IO_RESOURCES.resolve("semantic-routes.xml"))) { + RoutesDefinition routes = new ModelParser(input, NAMESPACE).parseRoutesDefinition().orElseThrow(); + String java = new LwModelToJavaDumper().dumpModelAsJava(null, routes); + assertTrue(java.contains("semanticQuestions().question(\"department\")")); + assertTrue(java.contains(".threshold(0.8)")); + List errors = compile("SemanticRoutes", wrapInRouteBuilder("SemanticRoutes", List.of(java))); + assertTrue(errors.isEmpty(), () -> errors + "\n" + java); + } + } + @ParameterizedTest(name = "{0}") @MethodSource("xmlRouteFiles") void xmlToJavaDslCompiles(String xmlFile) throws Exception { diff --git a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java index 10f7d4edf7366..b9f198f7f47b6 100644 --- a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java +++ b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java @@ -1049,6 +1049,7 @@ private Optional parseSingleRouteTemplatesDefinition() protected RoutesDefinition doParseRoutesDefinition() throws IOException, XmlPullParserException { return doParse(new RoutesDefinition(), optionalIdentifiedDefinitionAttributeHandler(), (def, key) -> switch (key) { case "route": doAdd(doParseRouteDefinition(), def.getRoutes(), def::setRoutes); yield true; + case "semantic": def.setSemantic(doParseSemanticDefinition()); yield true; default: yield optionalIdentifiedDefinitionElementHandler().accept(def, key); }, noValueHandler()); } @@ -1073,6 +1074,12 @@ private Optional parseSingleRoutesDefinition() throws IOExcept } return Optional.empty(); } + protected SemanticDefinition doParseSemanticDefinition() throws IOException, XmlPullParserException { + return doParse(new SemanticDefinition(), noAttributeHandler(), (def, key) -> switch (key) { + case "question": doAdd(doParseSemanticQuestionDefinition(), def.getQuestions(), def::setQuestions); yield true; + default: yield false; + }, noValueHandler()); + } protected RoutingSlipDefinition doParseRoutingSlipDefinition() throws IOException, XmlPullParserException { return doParse(new RoutingSlipDefinition(), (def, key, val) -> switch (key) { case "allowedSchemes": def.setAllowedSchemes(val); yield true; @@ -1430,6 +1437,7 @@ protected ElementHandler beansDefinitionElementHa case "routeConfiguration": doAdd(doParseRouteConfigurationDefinition(), def.getRouteConfigurations(), def::setRouteConfigurations); break; case "routeTemplate": doAdd(doParseRouteTemplateDefinition(), def.getRouteTemplates(), def::setRouteTemplates); break; case "route": doAdd(doParseRouteDefinition(), def.getRoutes(), def::setRoutes); break; + case "semantic": def.setSemantic(doParseSemanticDefinition()); break; case "sslContextParameters": doAdd(doParseSSLContextParametersDefinition(), def.getSslContextParameters(), def::setSslContextParameters); break; case "templatedRoute": doAdd(doParseTemplatedRouteDefinition(), def.getTemplatedRoutes(), def::setTemplatedRoutes); break; default: @@ -1545,6 +1553,22 @@ protected BeanPropertyDefinition doParseBeanPropertyDefinition() throws IOExcept default: yield false; }, noValueHandler()); } + protected SemanticQuestionDefinition doParseSemanticQuestionDefinition() throws IOException, XmlPullParserException { + return doParse(new SemanticQuestionDefinition(), (def, key, val) -> switch (key) { + case "name": def.setName(val); yield true; + case "state": def.setState(val); yield true; + case "threshold": def.setThreshold(val); yield true; + case "type": def.setType(val); yield true; + case "uncertainty": def.setUncertainty(val); yield true; + case "uncertaintyPolicy": def.setUncertaintyPolicy(val); yield true; + default: yield false; + }, (def, key) -> switch (key) { + case "criterion": doAdd(doParsePropertyDefinition(), def.getCriteria(), def::setCriteria); yield true; + case "instructions": def.setInstructions(doParseText()); yield true; + case "level": doAdd(doParseText(), def.getLevels(), def::setLevels); yield true; + default: yield false; + }, noValueHandler()); + } protected BatchResequencerConfig doParseBatchResequencerConfig() throws IOException, XmlPullParserException { return doParse(new BatchResequencerConfig(), (def, key, val) -> switch (key) { case "allowDuplicates": def.setAllowDuplicates(val); yield true; diff --git a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java index cf985566ea488..de883785f7719 100644 --- a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java +++ b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java @@ -1685,6 +1685,7 @@ protected void doWriteRoutesDefinition(String name, RoutesDefinition def) throws startElement(name); doWriteOptionalIdentifiedDefinitionAttributes(def); doWriteList(null, null, def.getRoutes(), this::doWriteRouteDefinitionRef); + doWriteElement("semantic", def.getSemantic(), this::doWriteSemanticDefinition); endElement(name); } protected void doWriteRoutingSlipDefinition(String name, RoutingSlipDefinition def) throws IOException { @@ -2082,6 +2083,7 @@ protected void doWriteBeanPropertyDefinition(String name, BeanPropertyDefinition protected void doWriteBeansDefinitionElements(BeansDefinition def) throws IOException { doWriteList(null, "component-scan", def.getComponentScanning(), this::doWriteComponentScanDefinition); doWriteList(null, "bean", def.getBeans(), this::doWriteBeanFactoryDefinition); + doWriteElement("semantic", def.getSemantic(), this::doWriteSemanticDefinition); domElements(def.getSpringOrBlueprintBeans()); doWriteList(null, "sslContextParameters", def.getSslContextParameters(), this::doWriteSSLContextParametersDefinition); doWriteList("dataFormats", "dataFormat", def.getDataFormats(), this::doWriteDataFormatDefinition); @@ -2132,6 +2134,24 @@ protected void doWriteSSLContextParametersDefinition(String name, SSLContextPara doWriteAttribute("clientAuthentication", def.getClientAuthentication(), "NONE"); endElement(name); } + protected void doWriteSemanticDefinition(String name, SemanticDefinition def) throws IOException { + startElement(name); + doWriteList(null, "question", def.getQuestions(), this::doWriteSemanticQuestionDefinition); + endElement(name); + } + protected void doWriteSemanticQuestionDefinition(String name, SemanticQuestionDefinition def) throws IOException { + startElement(name); + doWriteAttribute("name", def.getName(), null); + doWriteAttribute("type", def.getType(), null); + doWriteAttribute("state", def.getState(), null); + doWriteAttribute("threshold", def.getThreshold(), null); + doWriteAttribute("uncertainty", def.getUncertainty(), null); + doWriteAttribute("uncertaintyPolicy", def.getUncertaintyPolicy(), null); + doWriteElement("instructions", def.getInstructions(), this::doWriteString); + doWriteList(null, "criterion", def.getCriteria(), this::doWritePropertyDefinition); + doWriteList(null, "level", def.getLevels(), this::doWriteString); + endElement(name); + } protected void doWriteBatchResequencerConfig(String name, BatchResequencerConfig def) throws IOException { startElement(name); doWriteAttribute("batchSize", def.getBatchSize(), "100"); diff --git a/core/camel-xml-io/src/test/resources/beans-semantic.xml b/core/camel-xml-io/src/test/resources/beans-semantic.xml new file mode 100644 index 0000000000000..dae4972b5c2ca --- /dev/null +++ b/core/camel-xml-io/src/test/resources/beans-semantic.xml @@ -0,0 +1,39 @@ + + + + + + Is this urgent? + + + + + Which department? + + + + + How urgent? + Routine + Urgent + Critical + + + diff --git a/core/camel-xml-io/src/test/resources/semantic-routes.xml b/core/camel-xml-io/src/test/resources/semantic-routes.xml new file mode 100644 index 0000000000000..1dc074d6871dc --- /dev/null +++ b/core/camel-xml-io/src/test/resources/semantic-routes.xml @@ -0,0 +1,43 @@ + + + + + + ref:department + + + + Is this urgent? + + + + + Which department? + + + + + How urgent? + Routine + Urgent + Critical + + + diff --git a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java index 0753ef039cb4c..694a6c3894545 100644 --- a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java +++ b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java @@ -1681,6 +1681,7 @@ protected JsonObject doWriteRoutesDefinition(RoutesDefinition def) { JsonObject jo = new JsonObject(); doWriteOptionalIdentifiedDefinitionAttributes(jo, def); doWriteChildList(jo, "routes", "routes", def.getRoutes(), this::doWriteRouteDefinition); + doWriteChildElement(jo, "semantic", def.getSemantic(), this::doWriteSemanticDefinition); return jo; } protected JsonObject doWriteRoutingSlipDefinition(RoutingSlipDefinition def) { @@ -2078,6 +2079,7 @@ protected JsonObject doWriteBeanPropertyDefinition(BeanPropertyDefinition def) { protected void doWriteBeansDefinitionElements(JsonObject jo, BeansDefinition def) { doWriteChildList(jo, null, "component-scan", def.getComponentScanning(), this::doWriteComponentScanDefinition); doWriteChildList(jo, null, "bean", def.getBeans(), this::doWriteBeanFactoryDefinition); + doWriteChildElement(jo, "semantic", def.getSemantic(), this::doWriteSemanticDefinition); // @XmlAnyElement - not applicable for YAML doWriteChildList(jo, null, "sslContextParameters", def.getSslContextParameters(), this::doWriteSSLContextParametersDefinition); doWriteChildList(jo, "dataFormats", "dataFormat", def.getDataFormats(), this::doWriteDataFormatDefinition); diff --git a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java index 9d4f14b3e024c..e837a64d7710f 100644 --- a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java +++ b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java @@ -161,6 +161,9 @@ String resolve(String value, Properties properties) { List roots = new ArrayList<>(); try { if (definition instanceof RoutesDefinition rd) { + if (rd.getSemantic() != null) { + roots.add(writer.writeSemanticDefinition(rd.getSemantic())); + } for (RouteDefinition route : rd.getRoutes()) { roots.add(writer.writeRouteDefinition(route)); } diff --git a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java index 4fe49a1272052..d3a10a979e3da 100644 --- a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java +++ b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java @@ -25,6 +25,8 @@ import org.apache.camel.CamelContext; import org.apache.camel.catalog.RuntimeCamelCatalog; +import org.apache.camel.model.app.SemanticDefinition; +import org.apache.camel.model.app.SemanticQuestionDefinition; import org.apache.camel.util.URISupport; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; @@ -51,6 +53,44 @@ public void setCamelContext(CamelContext camelContext) { this.camelContext = camelContext; } + public JsonObject writeSemanticDefinition(SemanticDefinition definition) { + return wrapNode("semantic", doWriteSemanticDefinition(definition)); + } + + protected JsonObject doWriteSemanticDefinition(SemanticDefinition definition) { + JsonObject questions = new JsonObject(); + for (SemanticQuestionDefinition question : definition.getQuestions()) { + JsonObject value = new JsonObject(); + value.put("type", question.getType()); + value.put("instructions", question.getInstructions()); + if (question.getState() != null) { + value.put("state", question.getState()); + } + doWriteAttribute(value, "threshold", question.getThreshold(), null); + doWriteAttribute(value, "uncertainty", question.getUncertainty(), null); + doWriteAttribute(value, "uncertaintyPolicy", question.getUncertaintyPolicy(), null); + if (!question.getLevels().isEmpty()) { + JsonArray levels = new JsonArray(); + levels.addAll(question.getLevels()); + value.put("criteria", levels); + } else if (!question.getCriteria().isEmpty()) { + JsonObject criteria = new JsonObject(); + question.getCriteria().forEach(criterion -> { + if (criteria.putIfAbsent(criterion.getKey(), criterion.getValue()) != null) { + throw new IllegalArgumentException("Duplicate semantic criterion: " + criterion.getKey()); + } + }); + value.put("criteria", criteria); + } + if (questions.putIfAbsent(question.getName(), value) != null) { + throw new IllegalArgumentException("Duplicate semantic question: " + question.getName()); + } + } + JsonObject result = new JsonObject(); + result.put("question", questions); + return result; + } + protected void doWriteAttribute(JsonObject jo, String key, String value, String defaultValue) { if (value != null && (defaultValue == null || !defaultValue.equals(value))) { jo.put(key, parseValue(value)); diff --git a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlWriterEdgeCasesTest.java b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlWriterEdgeCasesTest.java index 7ce72aa8a0ebc..de449b3898662 100644 --- a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlWriterEdgeCasesTest.java +++ b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlWriterEdgeCasesTest.java @@ -23,15 +23,48 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import org.apache.camel.model.RouteDefinition; +import org.apache.camel.model.app.SemanticDefinition; import org.apache.camel.model.dataformat.JsonDataFormat; import org.apache.camel.util.json.JsonObject; import org.apache.camel.yaml.LwModelToYAMLDumper; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class YamlWriterEdgeCasesTest { + @Test + public void semanticExportRejectsDuplicatesInsteadOfDiscardingThem() { + SemanticDefinition questions = new SemanticDefinition(); + questions.question("q").type("boolean").instructions("First?"); + questions.question("q").type("boolean").instructions("Second?"); + YamlModelWriter writer = new YamlModelWriter(); + assertThatThrownBy(() -> writer.writeSemanticDefinition(questions)).hasMessageContaining("Duplicate semantic question"); + + SemanticDefinition criteria = new SemanticDefinition(); + criteria.question("q").type("choice").instructions("Which?").criterion("a", "First").criterion("a", "Second"); + assertThatThrownBy(() -> writer.writeSemanticDefinition(criteria)).hasMessageContaining("Duplicate semantic criterion"); + } + + @Test + public void semanticDeclarationsRetainYamlShapeAndTextTypes() throws Exception { + SemanticDefinition semantic = new SemanticDefinition(); + semantic.question("007").type("boolean").instructions("true").state("42").threshold(0.8) + .uncertainty(0.1).uncertaintyPolicy("non-match").criterion("true", "yes"); + semantic.question("priority").type("score").instructions("Priority?").level("Low").level("High"); + YamlModelWriter writer = new YamlModelWriter(); + String yaml = writer.printAsYaml(List.of(writer.writeSemanticDefinition(semantic))); + JsonNode node = new YAMLMapper().readTree(yaml); + JsonNode questions = node.get(0).get("semantic").get("question"); + assertThat(questions.isObject()).isTrue(); + assertThat(questions.get("007").get("instructions").isTextual()).isTrue(); + assertThat(questions.get("007").get("state").isTextual()).isTrue(); + assertThat(questions.get("007").get("criteria").get("true").asText()).isEqualTo("yes"); + assertThat(questions.get("007").get("threshold").doubleValue()).isEqualTo(0.8); + assertThat(questions.get("priority").get("criteria").get(1).asText()).isEqualTo("High"); + } + @Test public void testTextThatLooksLikeNumber() throws Exception { RouteDefinition route = new RouteDefinition(); diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java index 9e4b256f1b22c..8666b7206d00c 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java @@ -126,6 +126,9 @@ private String transformXmlToYaml(String xml) throws Exception { YamlModelWriter writer = new YamlModelWriter(); List roots = new ArrayList<>(); + if (routes.getSemantic() != null) { + roots.add(writer.writeSemanticDefinition(routes.getSemantic())); + } for (RouteDefinition route : routes.getRoutes()) { roots.add(writer.writeRouteDefinition(route)); } diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java index 385d19f1019d3..54a613b56314d 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java @@ -45,6 +45,20 @@ void transformXmlToYaml() { assertThat(result.result).contains("log"); } + @Test + void transformXmlToYamlPreservesSemanticDeclarations() { + var result = createTools().camel_transform_route( + """ + + Urgent? + ref:urgent + + """, + "xml", "yaml"); + assertThat(result.supported).isTrue(); + assertThat(result.result).contains("semantic:", "question:", "urgent:", "Urgent?", "ref:urgent"); + } + @Test void transformYamlToXml() { TransformTools tools = createTools(); diff --git a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java index 163e88b4501c9..8a44717b268a2 100644 --- a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java +++ b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java @@ -47,6 +47,7 @@ import org.apache.camel.model.TemplatedRoutesDefinition; import org.apache.camel.model.app.BeansDefinition; import org.apache.camel.model.app.SSLContextParametersDefinition; +import org.apache.camel.model.app.SemanticDefinition; import org.apache.camel.model.dataformat.DataFormatsDefinition; import org.apache.camel.model.rest.RestConfigurationDefinition; import org.apache.camel.model.rest.RestDefinition; @@ -75,6 +76,7 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { private final Map resourceCache = new ConcurrentHashMap<>(); private final Map xmlInfoCache = new ConcurrentHashMap<>(); private final Map camelAppCache = new ConcurrentHashMap<>(); + private final Map routesCache = new ConcurrentHashMap<>(); private final List> delayedRegistrations = new ArrayList<>(); private final AtomicInteger counter = new AtomicInteger(0); @@ -95,19 +97,50 @@ public void preParseRoute(Resource resource) throws Exception { if (preparseDone.getOrDefault(resource.getLocation(), false)) { return; } - XmlStreamInfo xmlInfo = xmlInfo(resource); - if (xmlInfo.isValid()) { - String root = xmlInfo.getRootElementName(); - if ("beans".equals(root) || "blueprint".equals(root) || "camel".equals(root)) { - new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) - .parseBeansDefinition() - .ifPresent(bd -> { - registerBeans(resource, bd); - camelAppCache.put(resource.getLocation(), bd); - }); + try { + XmlStreamInfo xmlInfo = xmlInfo(resource); + if (xmlInfo.isValid()) { + String root = xmlInfo.getRootElementName(); + SemanticDefinition semantic = null; + if ("beans".equals(root) || "blueprint".equals(root) || "camel".equals(root)) { + new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) + .parseBeansDefinition() + .ifPresent(bd -> { + registerBeans(resource, bd); + camelAppCache.put(resource.getLocation(), bd); + }); + BeansDefinition app = camelAppCache.get(resource.getLocation()); + if (app != null) { + semantic = app.getSemantic(); + } + } else if ("routes".equals(root) || "route".equals(root)) { + RoutesDefinition routes = new XmlModelParser(resource(resource), xmlInfo.getRootElementNamespace()) + .parseRoutesDefinition().orElse(null); + if (routes != null) { + routesCache.put(resource.getLocation(), routes); + semantic = routes.getSemantic(); + } + } + SemanticDefinition.configure(getCamelContext(), resource, resource.getLocation(), semantic); } + preparseDone.put(resource.getLocation(), true); + } catch (Exception e) { + // A failed batch also prevents builders for earlier resources from clearing their cached input. + resourceCache.clear(); + xmlInfoCache.clear(); + camelAppCache.clear(); + routesCache.clear(); + preparseDone.clear(); + throw e; } - preparseDone.put(resource.getLocation(), true); + } + + private void clearCaches(String location) { + resourceCache.remove(location); + xmlInfoCache.remove(location); + camelAppCache.remove(location); + routesCache.remove(location); + preparseDone.remove(location); } @Override @@ -150,19 +183,22 @@ public void configure() throws Exception { case "rests", "rest" -> new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) .parseRestsDefinition() .ifPresent(this::addRests); - case "routes", "route" -> new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) - .parseRoutesDefinition() - .ifPresent(this::addRoutes); + case "routes", "route" -> { + RoutesDefinition routes = routesCache.get(resourceLocation); + if (routes != null) { + addRoutes(routes); + } else { + new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) + .parseRoutesDefinition().ifPresent(this::addRoutes); + } + } default -> { } } } finally { // knowing this is the last time an XML may have been parsed, we can clear the cache // (route may get reloaded later) - resourceCache.remove(resourceLocation); - xmlInfoCache.remove(resourceLocation); - camelAppCache.remove(resourceLocation); - preparseDone.remove(resourceLocation); + clearCaches(resourceLocation); } } @@ -197,6 +233,7 @@ public void configuration() throws Exception { } private void configureCamel(BeansDefinition app) { + getRouteCollection().setSemantic(app.getSemantic()); if (!delayedRegistrations.isEmpty()) { // some of the beans were not available yet, so we have to try register them now for (BeanFactoryDefinition def : delayedRegistrations) { @@ -303,6 +340,7 @@ private void configureCamel(BeansDefinition app) { CamelContextAware.trySetCamelContext(def, getCamelContext()); def.setResource(getResource()); def.setRoutes(app.getRoutes()); + def.setSemantic(app.getSemantic()); addRoutes(def); } } @@ -329,6 +367,7 @@ private void addRests(RestsDefinition list) { } private void addRoutes(RoutesDefinition routes) { + getRouteCollection().setSemantic(routes.getSemantic()); // xml routes must be prepared in the same way java-dsl (via RoutesDefinition) // so create a copy and use the fluent builder to add the route for (RouteDefinition route : routes.getRoutes()) { diff --git a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoaderTest.java b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoaderTest.java index 0add991d21c60..188a9e03e9e2c 100644 --- a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoaderTest.java +++ b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoaderTest.java @@ -20,12 +20,32 @@ import org.apache.camel.builder.RouteConfigurationBuilder; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.spi.Resource; +import org.apache.camel.support.PluginHelper; import org.apache.camel.support.ResourceHelper; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; public class XmlRoutesBuilderLoaderTest { + @Test + public void semanticDeclarationsRequireTheOptionalComponent() throws Exception { + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.start(); + Resource resource = ResourceHelper.fromString("questions.xml", """ + + + Valid? + + + """); + var error = assertThrows(IllegalArgumentException.class, + () -> PluginHelper.getRoutesLoader(context).loadRoutes(resource)); + assertEquals("Semantic question declarations require camel-semantic on the classpath", error.getMessage()); + } + } + @Test public void canLoadRoutes() throws Exception { String content = "" diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/pom.xml b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/pom.xml index 1b4947b1ac581..6b731bf43035f 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/pom.xml +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/pom.xml @@ -103,6 +103,8 @@ org.apache.camel.model.RouteConfigurationsDefinition org.apache.camel.model.app.BeansDefinition org.apache.camel.model.app.ApplicationDefinition + org.apache.camel.model.app.SemanticDefinition + org.apache.camel.model.app.SemanticQuestionDefinition diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml b/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml index ab1611cab578a..7a7e023271f7d 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml @@ -242,6 +242,11 @@ camel-semantic test + + org.apache.camel + camel-yaml-io + test + diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java index 7008fde6d70e8..0a4414f552324 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java @@ -23,11 +23,15 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; +import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.mock.MockEndpoint; import org.apache.camel.dsl.yaml.common.exception.InvalidEnumException; import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException; import org.apache.camel.dsl.yaml.support.YamlTestSupport; import org.apache.camel.language.semantic.SemanticLanguage; +import org.apache.camel.model.RoutesDefinition; +import org.apache.camel.model.app.SemanticDefinition; +import org.apache.camel.model.language.LanguageExpression; import org.apache.camel.semantic.SemanticAdapter; import org.apache.camel.semantic.SemanticQuestion; import org.apache.camel.semantic.SemanticQuestions; @@ -36,6 +40,7 @@ import org.apache.camel.support.PluginHelper; import org.apache.camel.support.ResourceHelper; import org.apache.camel.support.RouteWatcherReloadStrategy; +import org.apache.camel.yaml.LwModelToYAMLDumper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; @@ -82,6 +87,59 @@ private static String declarations(String state) { """.formatted(state); } + @Test + void exportedModelLoadsWithTheExistingYamlSyntax() throws Exception { + SemanticDefinition semantic = new SemanticDefinition(); + semantic.question("urgent").type("boolean").instructions("Urgent?").threshold(0.8) + .uncertainty(0.1).uncertaintyPolicy("non-match"); + semantic.question("department").type("choice").state("${header.selected}").instructions("Which department?") + .criterion("billing", "Invoices").criterion("technical", "Outages"); + semantic.question("priority").type("score").instructions("Priority?").level("Low").level("High"); + RoutesDefinition routes = new RoutesDefinition(); + routes.setSemantic(semantic); + routes.from("direct:exported").setBody(new LanguageExpression("semantic", "ref:department")); + loadRoutes(new LwModelToYAMLDumper().dumpModelAsYaml(context, routes)); + context.start(); + assertThat(calls).hasValue(0); + var questions = SemanticQuestions.get(context); + assertThat(questions.get("urgent").getThreshold()).isEqualTo(0.8); + assertThat(questions.get("urgent").getUncertaintyPolicy()).isEqualTo(SemanticQuestion.UncertaintyPolicy.NON_MATCH); + assertThat(questions.get("priority").getLevels()).containsExactly("Low", "High"); + try (var template = context.createProducerTemplate()) { + assertThat(template.requestBodyAndHeader("direct:exported", "original", "selected", "invoice")) + .isEqualTo("billing"); + } + } + + @Test + void javaDeclarationsCoexistWithUnchangedYamlDeclarations() throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + semanticQuestions().question("javaDepartment").type("choice").state("${header.selected}") + .instructions("Which department?") + .criterion("billing", "Invoices and refunds").criterion("technical", "Bugs and outages"); + } + }); + loadRoutes(declarations("${header.selected}") + route()); + context.start(); + SemanticQuestion java = SemanticQuestions.get(context).get("javaDepartment"); + SemanticQuestion yaml = SemanticQuestions.get(context).get("department"); + assertThat(java).usingRecursiveComparison().isEqualTo(yaml); + assertThat(calls).hasValue(0); + try (var template = context.createProducerTemplate()) { + var exchange = template.request("direct:tickets", e -> { + e.getMessage().setBody("original"); + e.getMessage().setHeader("selected", "invoice"); + }); + assertThat(exchange.getException()).isNull(); + assertThat(context.resolveLanguage("semantic").createExpression("refs:javaDepartment,department") + .evaluate(exchange, Map.class)).containsEntry("javaDepartment", "billing") + .containsEntry("department", "billing"); + } + assertThat(selected).isEqualTo("invoice"); + } + @Test void ordinaryResourceDoesNotCreateSemanticQuestionState() throws Exception { loadRoutes(""" diff --git a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-java-dsl-writer.vm b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-java-dsl-writer.vm index ac93150b1b3d3..f195b5d3fbe11 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-java-dsl-writer.vm +++ b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-java-dsl-writer.vm @@ -89,7 +89,8 @@ public class JavaDslModelWriter extends JavaDslModelWriterSupport { #if( $clazz.simpleName == "OptionalIdentifiedDefinition" ) #set( $foo = $elementRefs.add($clazz) ) #end - #if( !$mojo.getXmlEnum($clazz) && !$clazz.isInterface() ) + #if( !$mojo.getXmlEnum($clazz) && !$clazz.isInterface() + && $clazz.simpleName != "SemanticDefinition" && $clazz.simpleName != "SemanticQuestionDefinition" ) #set( $hasDerived = false ) #foreach( $c in $model ) #if( $c.getSuperclass() == $clazz ) diff --git a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-yaml-writer.vm b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-yaml-writer.vm index 8b3da44d0e4ce..57b48a55174b0 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-yaml-writer.vm +++ b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-yaml-writer.vm @@ -83,7 +83,8 @@ public class YamlModelWriter extends YamlModelWriterSupport { #if( $clazz.simpleName == "OptionalIdentifiedDefinition" ) #set( $foo = $elementRefs.add($clazz) ) #end - #if( !$mojo.getXmlEnum($clazz) && !$clazz.isInterface() ) + #if( !$mojo.getXmlEnum($clazz) && !$clazz.isInterface() + && $clazz.simpleName != "SemanticDefinition" && $clazz.simpleName != "SemanticQuestionDefinition" ) #set( $hasDerived = false ) #foreach( $c in $model ) #if( $c.getSuperclass() == $clazz ) From 52866e6a63c4e4f433d4b760a1f36a309fa722e2 Mon Sep 17 00:00:00 2001 From: Luigi De Masi Date: Tue, 29 Sep 2026 13:13:43 +0200 Subject: [PATCH 02/11] CAMEL-25138: Align semantic XML schema order and declaration ownership Order semantic declarations before routes in the generated schemas and XML writer, with schema validation coverage for routes and camel documents. Use the supplied source consistently for question ownership, document the reserved model: prefix, and remove redundant XML declaration assignments. Document Java builder ordering and the runtime route dump limitation: direct model exports preserve declarations, but runtime dumps omit them. Explain the handwritten semantic serializers in the generator templates. XML resources are parsed during preparse so shared questions exist before route initialization. The loader's pending-cache invalidation is retained because a failed batch prevents earlier builders from clearing their input; retrying must read corrected resources. Validation: 928 focused tests passed, 2 skipped; full repository clean install with tests skipped; generated XML IO and Spring schema validation; formatter validation and import-order checks. Co-authored-by: Codex Signed-off-by: Luigi De Masi --- .../camel/catalog/docs/semantic-language.adoc | 22 +++++++++++++++---- .../camel/catalog/schemas/camel-spring.xsd | 2 +- .../camel/catalog/schemas/camel-xml-io.xsd | 2 +- .../src/main/docs/semantic-language.adoc | 22 +++++++++++++++---- .../DefaultSemanticDefinitionConfigurer.java | 8 ++----- .../camel/semantic/SemanticQuestions.java | 8 +++++-- .../semantic/SemanticDeclarationDslTest.java | 18 +++++++++++++++ .../apache/camel/model/RoutesDefinition.java | 2 ++ .../app/SemanticDefinitionConfigurer.java | 5 ++++- .../camel/java/out/JavaDslModelWriter.java | 2 +- .../org/apache/camel/xml/out/ModelWriter.java | 2 +- .../apache/camel/xml/out/ModelWriterTest.java | 22 +++++++++++++++++++ .../src/test/resources/semantic-routes.xml | 8 +++---- .../camel/yaml/out/YamlModelWriter.java | 2 +- .../dsl/xml/io/XmlRoutesBuilderLoader.java | 5 +++-- .../velocity/model-java-dsl-writer.vm | 1 + .../resources/velocity/model-yaml-writer.vm | 1 + 17 files changed, 104 insertions(+), 28 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 955e180b4208a..b2dd15c021d42 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 @@ -121,16 +121,23 @@ public void configure() { ---- Declarations are installed after `configure()` completes and before the routes are initialized. -They can appear before or after the routes that reference them. Use `.type("boolean")` with +They can appear before or after the routes that reference them within the same builder. +When adding builders to a running context, add the builder declaring shared questions before +builders whose routes reference them. Use `.type("boolean")` with optional `.threshold(0.8)`, `.uncertainty(0.1)` and `.uncertaintyPolicy("non-match")` for boolean questions. Use `.type("score")` and successive `.level("description")` calls for ordered score levels. Java applications can also continue to register immutable `SemanticQuestion` definitions using `SemanticQuestions.get(context).replace(source, questions)`. +Java/XML declarations reserve source keys beginning with `model:`. For a route resource, the +key is `"model:" + resource.getLocation()`; passing this key and an empty map to `replace` +removes that resource's questions. Reloading the resource installs them again. Embedded Java +builders without a resource receive distinct generated source keys. + === XML declarations With xref:others:java-xml-io-dsl.adoc[XML DSL], place a `semantic` element inside the `camel` or -`routes` document, alongside the routes: +`routes` document, before the routes to comply with the XML schema: [source,xml] ---- @@ -156,14 +163,21 @@ With xref:others:java-xml-io-dsl.adoc[XML DSL], place a `semantic` element insid For boolean questions, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional question attributes. For score questions, replace the named `criterion` elements with ordered `level` elements, such as `RoutineUrgentCritical`. -XML declarations are loaded before route references are initialized, including declarations -after a route or in another resource loaded together. Reload replaces the source's questions; +XML declarations are loaded before route references are initialized, including those in another +resource loaded together. Reload replaces the source's questions; removing its declarations or deleting the resource removes obsolete definitions. Both Java and XML declarations require `camel-semantic` and use the same context-wide registry, validation, defaults and adapters as YAML. They do not require `camel-yaml-dsl`. Their questions can also be selected together using `refs:name1,name2`, as described below. +=== Exporting declarations + +Direct exports of a `RoutesDefinition` containing semantic declarations preserve them, as does +XML-to-YAML conversion. Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only +the routes and omit the question declarations. Supply the declarations separately before +reloading those dumps. + === Selected state A question's optional `state` Simple expression overrides diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd index 6275aec276732..e30288e5c9899 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd @@ -12898,7 +12898,6 @@ Description of the template parameter for documentation purposes. - @@ -12908,6 +12907,7 @@ Named semantic question declarations. + diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd index 4dcbfe2ffdd24..63270474b9b60 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd @@ -12041,7 +12041,6 @@ Description of the template parameter for documentation purposes. - @@ -12051,6 +12050,7 @@ Named semantic question declarations. + 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 955e180b4208a..b2dd15c021d42 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 @@ -121,16 +121,23 @@ public void configure() { ---- Declarations are installed after `configure()` completes and before the routes are initialized. -They can appear before or after the routes that reference them. Use `.type("boolean")` with +They can appear before or after the routes that reference them within the same builder. +When adding builders to a running context, add the builder declaring shared questions before +builders whose routes reference them. Use `.type("boolean")` with optional `.threshold(0.8)`, `.uncertainty(0.1)` and `.uncertaintyPolicy("non-match")` for boolean questions. Use `.type("score")` and successive `.level("description")` calls for ordered score levels. Java applications can also continue to register immutable `SemanticQuestion` definitions using `SemanticQuestions.get(context).replace(source, questions)`. +Java/XML declarations reserve source keys beginning with `model:`. For a route resource, the +key is `"model:" + resource.getLocation()`; passing this key and an empty map to `replace` +removes that resource's questions. Reloading the resource installs them again. Embedded Java +builders without a resource receive distinct generated source keys. + === XML declarations With xref:others:java-xml-io-dsl.adoc[XML DSL], place a `semantic` element inside the `camel` or -`routes` document, alongside the routes: +`routes` document, before the routes to comply with the XML schema: [source,xml] ---- @@ -156,14 +163,21 @@ With xref:others:java-xml-io-dsl.adoc[XML DSL], place a `semantic` element insid For boolean questions, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional question attributes. For score questions, replace the named `criterion` elements with ordered `level` elements, such as `RoutineUrgentCritical`. -XML declarations are loaded before route references are initialized, including declarations -after a route or in another resource loaded together. Reload replaces the source's questions; +XML declarations are loaded before route references are initialized, including those in another +resource loaded together. Reload replaces the source's questions; removing its declarations or deleting the resource removes obsolete definitions. Both Java and XML declarations require `camel-semantic` and use the same context-wide registry, validation, defaults and adapters as YAML. They do not require `camel-yaml-dsl`. Their questions can also be selected together using `refs:name1,name2`, as described below. +=== Exporting declarations + +Direct exports of a `RoutesDefinition` containing semantic declarations preserve them, as does +XML-to-YAML conversion. Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only +the routes and omit the question declarations. Supply the declarations separately before +reloading those dumps. + === Selected state A question's optional `state` Simple expression overrides diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java index c6da03a7790ba..2d7b34e753743 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java @@ -48,12 +48,8 @@ public void configure(CamelContext context, Resource resource, String source, Se } } } - if (resource != null) { - // A YAML loader can register its own declarations from the same resource before its RouteBuilder runs. - SemanticQuestions.get(context).replace("model:" + resource.getLocation(), resource, questions); - } else { - SemanticQuestions.get(context).replace(source, questions); - } + // A YAML loader can register its own declarations from the same resource before its RouteBuilder runs. + SemanticQuestions.get(context).replace("model:" + source, resource, questions); } private static SemanticQuestion question(SemanticQuestionDefinition definition) { diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java index 5597e348ad5c0..cccc22e48ae62 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java @@ -43,7 +43,11 @@ public static SemanticQuestions get(CamelContext context) { } } - /** Replace all definitions from one source; an empty map removes obsolete declarations. */ + /** + * Replace all definitions from one source; an empty map removes obsolete declarations. Java/XML declarations + * reserve the {@code model:} prefix. Resource declarations use {@code model:} followed by + * {@link Resource#getLocation()}, while embedded Java builders use unique generated source keys. + */ public synchronized void replace(String source, Map definitions) { Map replacement = new HashMap<>(); sources.forEach((location, entries) -> { @@ -76,7 +80,7 @@ public synchronized void replace(Resource source, Map synchronized void replace(String location, Resource source, Map definitions) { removeDeletedResources(); replace(location, definitions); - if (!definitions.isEmpty() && "file".equals(source.getScheme())) { + if (source != null && !definitions.isEmpty() && "file".equals(source.getScheme())) { resources.put(location, source); } } diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java index 79b3ca75e1b6c..54bef60376438 100644 --- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java +++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java @@ -223,6 +223,24 @@ public void configure() { assertThat(states).isEmpty(); } + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void declarationSourceCanBeReplacedAndRemovedIndependentlyOfItsResource(boolean trackResource) { + Resource resource = trackResource ? ResourceHelper.fromString("questions.xml", "") : null; + SemanticDefinition definition = new SemanticDefinition(); + definition.question("first").type("boolean").instructions("Valid?"); + SemanticDefinition.configure(context, resource, "declared-source", definition); + SemanticDefinition replacement = new SemanticDefinition(); + replacement.question("second").type("boolean").instructions("Updated?"); + SemanticDefinition.configure(context, resource, "declared-source", replacement); + SemanticQuestions questions = SemanticQuestions.get(context); + assertThatThrownBy(() -> questions.get("first")).hasMessageContaining("Unknown"); + assertThat(questions.get("second")).isNotNull(); + questions.replace("model:declared-source", Map.of()); + assertThatThrownBy(() -> questions.get("second")).hasMessageContaining("Unknown"); + assertThat(states).isEmpty(); + } + @Test void embeddedBuilderInstancesOwnSeparateDeclarations() throws Exception { class Questions extends RouteBuilder { diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java index 9ca2b1b5b6305..f1ae6086fb01d 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java @@ -27,6 +27,7 @@ import jakarta.xml.bind.annotation.XmlElementRef; import jakarta.xml.bind.annotation.XmlRootElement; import jakarta.xml.bind.annotation.XmlTransient; +import jakarta.xml.bind.annotation.XmlType; import org.apache.camel.CamelContext; import org.apache.camel.Endpoint; @@ -50,6 +51,7 @@ */ @Metadata(label = "configuration", description = "Container for a collection of route definitions") @XmlRootElement(name = "routes") +@XmlType(propOrder = { "semantic", "routes" }) @XmlAccessorType(XmlAccessType.FIELD) public class RoutesDefinition extends OptionalIdentifiedDefinition implements RouteContainer, ResourceAware { diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinitionConfigurer.java b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinitionConfigurer.java index 141ba19a93236..49977893df9df 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinitionConfigurer.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinitionConfigurer.java @@ -21,6 +21,9 @@ /** Installs semantic declarations using the optional semantic language module. */ public interface SemanticDefinitionConfigurer { - /** Replace the questions belonging to this source, including removing obsolete declarations. */ + /** + * Replace the questions belonging to this source, including removing obsolete declarations. The source identifies + * the owner independently of the optional resource used to track deletion. + */ void configure(CamelContext context, Resource resource, String source, SemanticDefinition definition); } diff --git a/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java b/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java index 42843438f19a6..6fd7e0ef94561 100644 --- a/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java +++ b/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java @@ -2473,8 +2473,8 @@ protected void doWriteRouteTemplatesDefinition(StringBuilder sb, RouteTemplatesD } protected void doWriteRoutesDefinition(StringBuilder sb, RoutesDefinition def) { doWriteOptionalIdentifiedDefinitionAttributes(sb, def); - doWriteChildList(sb, "routes", def.getRoutes(), this::doWriteRouteDefinitionRef); doWriteChildElement(sb, "semantic", def.getSemantic(), this::doWriteSemanticDefinition); + doWriteChildList(sb, "routes", def.getRoutes(), this::doWriteRouteDefinitionRef); } protected void doWriteRoutingSlipDefinition(StringBuilder sb, RoutingSlipDefinition def) { doWriteProcessorDefinitionAttributes(sb, def); diff --git a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java index de883785f7719..d6a50e2a08bf8 100644 --- a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java +++ b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java @@ -1684,8 +1684,8 @@ protected void doWriteRouteTemplatesDefinition(String name, RouteTemplatesDefini protected void doWriteRoutesDefinition(String name, RoutesDefinition def) throws IOException { startElement(name); doWriteOptionalIdentifiedDefinitionAttributes(def); - doWriteList(null, null, def.getRoutes(), this::doWriteRouteDefinitionRef); doWriteElement("semantic", def.getSemantic(), this::doWriteSemanticDefinition); + doWriteList(null, null, def.getRoutes(), this::doWriteRouteDefinitionRef); endElement(name); } protected void doWriteRoutingSlipDefinition(String name, RoutingSlipDefinition def) throws IOException { diff --git a/core/camel-xml-io/src/test/java/org/apache/camel/xml/out/ModelWriterTest.java b/core/camel-xml-io/src/test/java/org/apache/camel/xml/out/ModelWriterTest.java index d1f86d704e60b..8760bdd918484 100644 --- a/core/camel-xml-io/src/test/java/org/apache/camel/xml/out/ModelWriterTest.java +++ b/core/camel-xml-io/src/test/java/org/apache/camel/xml/out/ModelWriterTest.java @@ -37,6 +37,10 @@ import jakarta.xml.bind.annotation.XmlTransient; +import javax.xml.XMLConstants; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.SchemaFactory; + import org.w3c.dom.Element; import org.apache.camel.model.RouteTemplatesDefinition; @@ -51,6 +55,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.xmlunit.assertj3.XmlAssert; import org.xmlunit.diff.DefaultNodeMatcher; import org.xmlunit.diff.ElementSelectors; @@ -68,6 +73,23 @@ public class ModelWriterTest { private static final Map TRANSIENT = new ConcurrentHashMap<>(); + @ParameterizedTest + @ValueSource(strings = { "routes", "camel" }) + void semanticDeclarationsBeforeRoutesConformToTheSchema(String root) throws Exception { + String xml = """ + <%s xmlns="http://camel.apache.org/schema/spring"> + + Valid? + + + + """.formatted(root, root); + // The build changes the generated schema's namespace to xml-io after the test phase. + SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) + .newSchema(getClass().getResource("/camel-xml-io.xsd")) + .newValidator().validate(new StreamSource(new StringReader(xml))); + } + @ParameterizedTest @MethodSource("routes") @DisplayName("Test xml roundtrip for ") diff --git a/core/camel-xml-io/src/test/resources/semantic-routes.xml b/core/camel-xml-io/src/test/resources/semantic-routes.xml index 1dc074d6871dc..061c9cda2e9fe 100644 --- a/core/camel-xml-io/src/test/resources/semantic-routes.xml +++ b/core/camel-xml-io/src/test/resources/semantic-routes.xml @@ -18,10 +18,6 @@ --> - - - ref:department - Is this urgent? @@ -40,4 +36,8 @@ Critical + + + ref:department + diff --git a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java index 694a6c3894545..2ee4afc192f78 100644 --- a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java +++ b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java @@ -1680,8 +1680,8 @@ protected JsonObject doWriteRouteTemplatesDefinition(RouteTemplatesDefinition de protected JsonObject doWriteRoutesDefinition(RoutesDefinition def) { JsonObject jo = new JsonObject(); doWriteOptionalIdentifiedDefinitionAttributes(jo, def); - doWriteChildList(jo, "routes", "routes", def.getRoutes(), this::doWriteRouteDefinition); doWriteChildElement(jo, "semantic", def.getSemantic(), this::doWriteSemanticDefinition); + doWriteChildList(jo, "routes", "routes", def.getRoutes(), this::doWriteRouteDefinition); return jo; } protected JsonObject doWriteRoutingSlipDefinition(RoutingSlipDefinition def) { diff --git a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java index 8a44717b268a2..a945d754496fd 100644 --- a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java +++ b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java @@ -340,7 +340,6 @@ private void configureCamel(BeansDefinition app) { CamelContextAware.trySetCamelContext(def, getCamelContext()); def.setResource(getResource()); def.setRoutes(app.getRoutes()); - def.setSemantic(app.getSemantic()); addRoutes(def); } } @@ -367,7 +366,9 @@ private void addRests(RestsDefinition list) { } private void addRoutes(RoutesDefinition routes) { - getRouteCollection().setSemantic(routes.getSemantic()); + if (routes.getSemantic() != null) { + getRouteCollection().setSemantic(routes.getSemantic()); + } // xml routes must be prepared in the same way java-dsl (via RoutesDefinition) // so create a copy and use the fluent builder to add the route for (RouteDefinition route : routes.getRoutes()) { diff --git a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-java-dsl-writer.vm b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-java-dsl-writer.vm index f195b5d3fbe11..6d9a89ca0632a 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-java-dsl-writer.vm +++ b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-java-dsl-writer.vm @@ -89,6 +89,7 @@ public class JavaDslModelWriter extends JavaDslModelWriterSupport { #if( $clazz.simpleName == "OptionalIdentifiedDefinition" ) #set( $foo = $elementRefs.add($clazz) ) #end +## Semantic declaration writers are hand-written in JavaDslModelWriterSupport. #if( !$mojo.getXmlEnum($clazz) && !$clazz.isInterface() && $clazz.simpleName != "SemanticDefinition" && $clazz.simpleName != "SemanticQuestionDefinition" ) #set( $hasDerived = false ) diff --git a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-yaml-writer.vm b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-yaml-writer.vm index 57b48a55174b0..09047eaed9bbf 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-yaml-writer.vm +++ b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-yaml-writer.vm @@ -83,6 +83,7 @@ public class YamlModelWriter extends YamlModelWriterSupport { #if( $clazz.simpleName == "OptionalIdentifiedDefinition" ) #set( $foo = $elementRefs.add($clazz) ) #end +## Semantic declaration writers are hand-written in YamlModelWriterSupport. #if( !$mojo.getXmlEnum($clazz) && !$clazz.isInterface() && $clazz.simpleName != "SemanticDefinition" && $clazz.simpleName != "SemanticQuestionDefinition" ) #set( $hasDerived = false ) From b6a0e7ed2db98b4d1d3f1e43ca2ac064d1b532b2 Mon Sep 17 00:00:00 2001 From: Luigi De Masi Date: Tue, 29 Sep 2026 14:30:32 +0200 Subject: [PATCH 03/11] CAMEL-25138: Fix semantic XML documentation and numeric diagnostics Use the XML IO namespace for the native XML declaration example so the catalog documentation schema check validates it, and regenerate its mirror. Identify threshold and uncertainty in non-numeric validation errors while preserving the question context and original cause. Cover both fields in the existing invalid-reload tests, including preservation of the previous definitions and recovery after corrected input. Clarify why duplicate discovery of the stateless default configurer is harmless. Validation: 109 semantic tests and 1082 catalog tests passed, including the documentation schema checks. Full 695-module clean install passed with tests skipped. Co-authored-by: Codex Signed-off-by: Luigi De Masi --- .../apache/camel/catalog/docs/semantic-language.adoc | 2 +- .../src/main/docs/semantic-language.adoc | 2 +- .../DefaultSemanticDefinitionConfigurer.java | 12 ++++++++++-- .../camel/semantic/SemanticDeclarationDslTest.java | 4 ++++ .../apache/camel/model/app/SemanticDefinition.java | 2 ++ 5 files changed, 18 insertions(+), 4 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 b2dd15c021d42..c360bacb879c5 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 @@ -141,7 +141,7 @@ With xref:others:java-xml-io-dsl.adoc[XML DSL], place a `semantic` element insid [source,xml] ---- - + Which department should handle this message? 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 b2dd15c021d42..c360bacb879c5 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 @@ -141,7 +141,7 @@ With xref:others:java-xml-io-dsl.adoc[XML DSL], place a `semantic` element insid [source,xml] ---- - + Which department should handle this message? diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java index 2d7b34e753743..871f378049fb5 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java @@ -72,13 +72,21 @@ private static SemanticQuestion question(SemanticQuestionDefinition definition) return new SemanticQuestion( type, definition.getInstructions(), definition.getState(), criteria, definition.getLevels(), - definition.getThreshold() == null ? 0.5 : Double.parseDouble(definition.getThreshold()), - definition.getUncertainty() == null ? 0 : Double.parseDouble(definition.getUncertainty()), + definition.getThreshold() == null ? 0.5 : parseDouble(definition.getThreshold(), "threshold"), + definition.getUncertainty() == null ? 0 : parseDouble(definition.getUncertainty(), "uncertainty"), definition.getUncertaintyPolicy() == null ? SemanticQuestion.UncertaintyPolicy.FAIL : enumeration(definition.getUncertaintyPolicy(), SemanticQuestion.UncertaintyPolicy.class)); } + private static double parseDouble(String value, String field) { + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(field + " must be a valid number: " + value, e); + } + } + private static > T enumeration(String value, Class type) { String normalized = StringHelper.asEnumConstantValue(value); for (T constant : type.getEnumConstants()) { diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java index 54bef60376438..e2b3f13a7f24b 100644 --- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java +++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java @@ -192,6 +192,10 @@ static Stream invalidXml() { "Duplicate semantic criterion"), Arguments.of(question.replace("type=\"boolean\"", "type=\"score\""), "score needs ordered levels"), Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" threshold=\"NaN\""), "within [0,1]"), + Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" threshold=\"abc\""), + "Invalid semantic question 'q': threshold must be a valid number: abc"), + Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" uncertainty=\"abc\""), + "Invalid semantic question 'q': uncertainty must be a valid number: abc"), Arguments.of(question.replace("type=\"boolean\"", "type=\"boolean\" state=\" \""), "state selector must not be blank"), Arguments.of(question.replace("name=\"q\"", "unknown=\"q\""), "Unexpected attribute")); diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java index 66ae92ca24961..ac1f2f50c0ddd 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java @@ -62,6 +62,8 @@ public static void configure( if (definition == null || definition.getQuestions().isEmpty()) { return; } + // Concurrent discovery may create equivalent instances of the stateless default configurer. + // The semantic module synchronizes access to shared question state. configurer = context.getCamelContextExtension().getDefaultFactoryFinder() .newInstance("semantic-configurer", SemanticDefinitionConfigurer.class) .orElseThrow(() -> new IllegalArgumentException( From 987488df11b07aa3b30bb3894cd237a1a07a324f Mon Sep 17 00:00:00 2001 From: Luigi De Masi Date: Tue, 29 Sep 2026 15:35:00 +0200 Subject: [PATCH 04/11] CAMEL-25138: Validate semantic schemas across build phases Read the generated schema's target namespace in the declaration-order test. A clean test build produces the Spring namespace, while an incremental build after packaging can retain the XML IO namespace. The hard-coded Spring namespace therefore failed in CI's Java 17 test phase. Accept only the two expected namespaces and keep schema validation for semantic declarations before routes under both routes and camel roots. Validation: reproduced the two CI failures on Java 17 before the fix. The XML IO suite passes on Java 17 with clean and packaged schemas and on Java 25 (425 tests per run, including two existing skips). The full 695-module clean install passes with tests skipped. All 11 CI-selected modules also pass on Java 17: 2928 tests, no failures or errors, and two existing skips. Co-authored-by: Codex Signed-off-by: Luigi De Masi --- .../org/apache/camel/xml/out/ModelWriterTest.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/core/camel-xml-io/src/test/java/org/apache/camel/xml/out/ModelWriterTest.java b/core/camel-xml-io/src/test/java/org/apache/camel/xml/out/ModelWriterTest.java index 8760bdd918484..7cc364e282de1 100644 --- a/core/camel-xml-io/src/test/java/org/apache/camel/xml/out/ModelWriterTest.java +++ b/core/camel-xml-io/src/test/java/org/apache/camel/xml/out/ModelWriterTest.java @@ -26,6 +26,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Collection; @@ -38,6 +39,7 @@ import jakarta.xml.bind.annotation.XmlTransient; import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.SchemaFactory; @@ -76,17 +78,21 @@ public class ModelWriterTest { @ParameterizedTest @ValueSource(strings = { "routes", "camel" }) void semanticDeclarationsBeforeRoutesConformToTheSchema(String root) throws Exception { + URL schema = getClass().getResource("/camel-xml-io.xsd"); + // Clean test builds use the Spring namespace; incremental builds can reuse the packaged xml-io schema. + String namespace = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(schema.toExternalForm()) + .getDocumentElement().getAttribute("targetNamespace"); + assertThat(namespace).isIn("http://camel.apache.org/schema/spring", NAMESPACE); String xml = """ - <%s xmlns="http://camel.apache.org/schema/spring"> + <%s xmlns="%s"> Valid? - """.formatted(root, root); - // The build changes the generated schema's namespace to xml-io after the test phase. + """.formatted(root, namespace, root); SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) - .newSchema(getClass().getResource("/camel-xml-io.xsd")) + .newSchema(schema) .newValidator().validate(new StreamSource(new StringReader(xml))); } From 02f4ad4b4f46ceaa0efaf0521b92832ddc29e7e9 Mon Sep 17 00:00:00 2001 From: Luigi De Masi Date: Tue, 29 Sep 2026 16:50:41 +0200 Subject: [PATCH 05/11] CAMEL-25138: Preserve semantic declarations across DSL loading and export Keep semantic declarations when loading JAXB routes and converting YAML or Java routes through MCP. Export registered question policies and prepare Java expression models without starting routes or invoking providers. Refresh XML preparse caches when resource content changes after a failed batch, while preserving unchanged preparse and deferred bean ownership. Add regression coverage for foreign-loader and earlier-builder failures. Preserve numeric placeholders in Java exports and resolve them before runtime validation. Add numeric catalog types and defaults, move optional component discovery into a helper, relocate the configurer SPI, and keep RoutesDefinition accessors together. Validation: 2978 tests across the affected Java 17 module suites, with no failures or errors and two existing skips. Full repository clean install with tests skipped passes on Java 25. Regenerated artifacts are included. Co-authored-by: Codex Signed-off-by: Luigi De Masi --- .../camel/catalog/docs/semantic-language.adoc | 15 +- .../semanticQuestionDefinition.json | 6 +- components/camel-ai/camel-semantic/pom.xml | 5 + .../src/main/docs/semantic-language.adoc | 15 +- .../DefaultSemanticDefinitionConfigurer.java | 37 ++++- .../camel/semantic/SemanticQuestions.java | 4 + .../yaml/SemanticDefinitionDeserializer.java | 18 ++- .../semantic/SemanticDeclarationDslTest.java | 140 +++++++++++++++++- .../model/app/semanticQuestionDefinition.json | 6 +- .../apache/camel/builder/RouteBuilder.java | 5 +- .../apache/camel/model/RoutesDefinition.java | 16 +- .../camel/model/SemanticDefinitionHelper.java | 48 ++++++ .../camel/model/app/SemanticDefinition.java | 22 --- .../model/app/SemanticQuestionDefinition.java | 20 ++- .../SemanticDefinitionConfigurer.java | 3 +- .../java/out/JavaDslModelWriterSupport.java | 4 +- .../camel/java/out/JavaDslCompileTest.java | 15 +- .../org/apache/camel/xml/out/ModelWriter.java | 6 +- dsl/camel-jbang/camel-jbang-mcp/pom.xml | 10 ++ .../core/commands/mcp/TransformTools.java | 14 ++ .../core/commands/mcp/TransformToolsTest.java | 70 +++++++++ .../dsl/xml/io/XmlRoutesBuilderLoader.java | 28 +++- .../xml/jaxb/JaxbXmlRoutesBuilderLoader.java | 1 + .../camel/dsl/yaml/SemanticQuestionTest.java | 15 ++ 24 files changed, 443 insertions(+), 80 deletions(-) create mode 100644 core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java rename core/camel-core-model/src/main/java/org/apache/camel/model/{app => spi}/SemanticDefinitionConfigurer.java (93%) 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 c360bacb879c5..06e37072ac63b 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 @@ -167,14 +167,25 @@ XML declarations are loaded before route references are initialized, including t resource loaded together. Reload replaces the source's questions; removing its declarations or deleting the resource removes obsolete definitions. +The JAXB XML loader also supports declarations inside a `routes` document using the +`http://camel.apache.org/schema/spring` namespace. With JAXB, load a separate declaration +resource before the routes that reference it. + +The numeric `threshold` and `uncertainty` options accept property placeholders in all three +DSLs. For example, Java accepts `.threshold("{{semantic.threshold:0.5}}")`, and XML accepts +`threshold="{{semantic.threshold:0.5}}"`. Values are resolved and validated when declarations +are registered; Java model exports preserve the placeholder text. + Both Java and XML declarations require `camel-semantic` and use the same context-wide registry, validation, defaults and adapters as YAML. They do not require `camel-yaml-dsl`. Their questions can also be selected together using `refs:name1,name2`, as described below. === Exporting declarations -Direct exports of a `RoutesDefinition` containing semantic declarations preserve them, as does -XML-to-YAML conversion. Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only +Direct exports of a `RoutesDefinition` containing semantic declarations preserve them. The MCP +route transformation tool also preserves declarations in XML-to-YAML, YAML-to-XML, and +Java-to-YAML/XML conversions. Conversions that load a Camel context export the registered +questions with their effective numeric policies. Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only the routes and omit the question declarations. Supply the declarations separately before reloading those dumps. diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticQuestionDefinition.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticQuestionDefinition.json index 12d4bbcbe2567..a530faab49795 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticQuestionDefinition.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticQuestionDefinition.json @@ -15,9 +15,9 @@ "name": { "index": 0, "kind": "attribute", "displayName": "Name", "group": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The context-wide question name." }, "type": { "index": 1, "kind": "attribute", "displayName": "Type", "group": "common", "required": true, "type": "enum", "javaType": "java.lang.String", "enum": [ "boolean", "choice", "score" ], "deprecated": false, "autowired": false, "secret": false, "description": "The question type." }, "state": { "index": 2, "kind": "attribute", "displayName": "State", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The Simple expression selecting the message state." }, - "threshold": { "index": 3, "kind": "attribute", "displayName": "Threshold", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The boolean decision threshold." }, - "uncertainty": { "index": 4, "kind": "attribute", "displayName": "Uncertainty", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The boolean uncertainty band." }, - "uncertaintyPolicy": { "index": 5, "kind": "attribute", "displayName": "Uncertainty Policy", "group": "common", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "fail", "non-match" ], "deprecated": false, "autowired": false, "secret": false, "description": "The boolean uncertainty policy." }, + "threshold": { "index": 3, "kind": "attribute", "displayName": "Threshold", "group": "common", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "0.5", "description": "The boolean decision threshold." }, + "uncertainty": { "index": 4, "kind": "attribute", "displayName": "Uncertainty", "group": "common", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "0", "description": "The boolean uncertainty band." }, + "uncertaintyPolicy": { "index": 5, "kind": "attribute", "displayName": "Uncertainty Policy", "group": "common", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "fail", "non-match" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "fail", "description": "The boolean uncertainty policy." }, "instructions": { "index": 6, "kind": "element", "displayName": "Instructions", "group": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Instructions describing the judgment to make." }, "criterion": { "index": 7, "kind": "element", "displayName": "Criterion", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Named choice criteria, or optional true\/false boolean criteria." }, "level": { "index": 8, "kind": "element", "displayName": "Level", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Ordered descriptive levels for a score question." } diff --git a/components/camel-ai/camel-semantic/pom.xml b/components/camel-ai/camel-semantic/pom.xml index 833a7d9174d36..25e764518c153 100644 --- a/components/camel-ai/camel-semantic/pom.xml +++ b/components/camel-ai/camel-semantic/pom.xml @@ -52,6 +52,11 @@ camel-xml-io-dsl test + + org.apache.camel + camel-xml-jaxb-dsl + test + org.apache.camel camel-main 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 c360bacb879c5..06e37072ac63b 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 @@ -167,14 +167,25 @@ XML declarations are loaded before route references are initialized, including t resource loaded together. Reload replaces the source's questions; removing its declarations or deleting the resource removes obsolete definitions. +The JAXB XML loader also supports declarations inside a `routes` document using the +`http://camel.apache.org/schema/spring` namespace. With JAXB, load a separate declaration +resource before the routes that reference it. + +The numeric `threshold` and `uncertainty` options accept property placeholders in all three +DSLs. For example, Java accepts `.threshold("{{semantic.threshold:0.5}}")`, and XML accepts +`threshold="{{semantic.threshold:0.5}}"`. Values are resolved and validated when declarations +are registered; Java model exports preserve the placeholder text. + Both Java and XML declarations require `camel-semantic` and use the same context-wide registry, validation, defaults and adapters as YAML. They do not require `camel-yaml-dsl`. Their questions can also be selected together using `refs:name1,name2`, as described below. === Exporting declarations -Direct exports of a `RoutesDefinition` containing semantic declarations preserve them, as does -XML-to-YAML conversion. Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only +Direct exports of a `RoutesDefinition` containing semantic declarations preserve them. The MCP +route transformation tool also preserves declarations in XML-to-YAML, YAML-to-XML, and +Java-to-YAML/XML conversions. Conversions that load a Camel context export the registered +questions with their effective numeric policies. Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only the routes and omit the question declarations. Supply the declarations separately before reloading those dumps. diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java index 871f378049fb5..9971fe18596d2 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java @@ -17,13 +17,14 @@ package org.apache.camel.semantic; import java.util.LinkedHashMap; +import java.util.Locale; import java.util.Map; import org.apache.camel.CamelContext; import org.apache.camel.model.PropertyDefinition; import org.apache.camel.model.app.SemanticDefinition; -import org.apache.camel.model.app.SemanticDefinitionConfigurer; import org.apache.camel.model.app.SemanticQuestionDefinition; +import org.apache.camel.model.spi.SemanticDefinitionConfigurer; import org.apache.camel.spi.Resource; import org.apache.camel.util.StringHelper; @@ -42,7 +43,7 @@ public void configure(CamelContext context, Resource resource, String source, Se throw new IllegalArgumentException("Duplicate semantic question: " + name); } try { - questions.put(name, question(question)); + questions.put(name, question(context, question)); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid semantic question '" + name + "': " + e.getMessage(), e); } @@ -52,7 +53,29 @@ public void configure(CamelContext context, Resource resource, String source, Se SemanticQuestions.get(context).replace("model:" + source, resource, questions); } - private static SemanticQuestion question(SemanticQuestionDefinition definition) { + /** Export a snapshot of the registered questions and their effective decision policies. */ + public static SemanticDefinition getDefinition(CamelContext context) { + SemanticQuestions questions = context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class); + if (questions == null) { + return null; + } + SemanticDefinition definition = new SemanticDefinition(); + questions.snapshot().entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> { + SemanticQuestion question = entry.getValue(); + SemanticQuestionDefinition target = definition.question(entry.getKey()) + .type(question.getType().name().toLowerCase(Locale.ROOT)) + .instructions(question.getInstructions()).state(question.getState()); + question.getCriteria().forEach(target::criterion); + question.getLevels().forEach(target::level); + if (question.getType() == SemanticQuestion.Type.BOOLEAN) { + target.threshold(question.getThreshold()).uncertainty(question.getUncertainty()) + .uncertaintyPolicy(question.getUncertaintyPolicy().name().toLowerCase(Locale.ROOT).replace('_', '-')); + } + }); + return definition; + } + + private static SemanticQuestion question(CamelContext context, SemanticQuestionDefinition definition) { if (definition.getType() == null) { throw new IllegalArgumentException("Question type is required"); } @@ -72,16 +95,16 @@ private static SemanticQuestion question(SemanticQuestionDefinition definition) return new SemanticQuestion( type, definition.getInstructions(), definition.getState(), criteria, definition.getLevels(), - definition.getThreshold() == null ? 0.5 : parseDouble(definition.getThreshold(), "threshold"), - definition.getUncertainty() == null ? 0 : parseDouble(definition.getUncertainty(), "uncertainty"), + definition.getThreshold() == null ? 0.5 : parseDouble(context, definition.getThreshold(), "threshold"), + definition.getUncertainty() == null ? 0 : parseDouble(context, definition.getUncertainty(), "uncertainty"), definition.getUncertaintyPolicy() == null ? SemanticQuestion.UncertaintyPolicy.FAIL : enumeration(definition.getUncertaintyPolicy(), SemanticQuestion.UncertaintyPolicy.class)); } - private static double parseDouble(String value, String field) { + private static double parseDouble(CamelContext context, String value, String field) { try { - return Double.parseDouble(value); + return Double.parseDouble(context.resolvePropertyPlaceholders(value)); } catch (NumberFormatException e) { throw new IllegalArgumentException(field + " must be a valid number: " + value, e); } diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java index cccc22e48ae62..5114f0107972e 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java @@ -90,6 +90,10 @@ synchronized void removeDeletedResources() { .forEach(location -> replace(location, Map.of())); } + Map snapshot() { + return questions; + } + public SemanticQuestion get(String name) { SemanticQuestion question = questions.get(name); if (question == null) { diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java index b4f049f8aca5b..f2cd3ef542484 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/yaml/SemanticDefinitionDeserializer.java @@ -57,7 +57,7 @@ public ConstructNode resolve(String id) { @Override public Object construct(Node node) { - read(node); + read(getDeserializationContext(node).getCamelContext(), node); // Registration happens once for the entire resource, including declarations after routes. return (CamelContextCustomizer) context -> { }; @@ -76,7 +76,7 @@ public void preParse(YamlDeserializationContext dc, Node root) { } for (NodeTuple tuple : mapping.getValue()) { if ("semantic".equals(asText(tuple.getKeyNode()))) { - read(tuple.getValueNode()).forEach((name, question) -> { + read(dc.getCamelContext(), tuple.getValueNode()).forEach((name, question) -> { if (definitions.putIfAbsent(name, question) != null) { throw new YamlDeserializationException( tuple.getValueNode(), "Duplicate semantic question: " + name); @@ -98,7 +98,7 @@ public void preParse(YamlDeserializationContext dc, Node root) { } } - private static Map read(Node node) { + private static Map read(CamelContext context, Node node) { Map semantic = fields(node, "semantic declaration"); if (!semantic.keySet().equals(Set.of("question"))) { throw new YamlDeserializationException(node, "Semantic declaration requires only question"); @@ -109,7 +109,7 @@ private static Map read(Node node) { throw new YamlDeserializationException(definition, "Semantic question requires a nonblank name"); } try { - result.put(name, readQuestion(name, definition)); + result.put(name, readQuestion(context, name, definition)); } catch (IllegalArgumentException e) { throw new YamlDeserializationException( definition, "Invalid semantic question '" + name + "': " + e.getMessage(), e); @@ -118,7 +118,7 @@ private static Map read(Node node) { return result; } - private static SemanticQuestion readQuestion(String name, Node definition) { + private static SemanticQuestion readQuestion(CamelContext context, String name, Node definition) { Map values = fields(definition, "semantic question '" + name + "'"); values.forEach((field, value) -> { if (!FIELDS.contains(field)) { @@ -152,7 +152,8 @@ private static SemanticQuestion readQuestion(String name, Node definition) { : SemanticQuestion.UncertaintyPolicy.FAIL; return new SemanticQuestion( type, asText(values.get("instructions")), asText(values.get("state")), - criteria, levels, number(values, name, "threshold", 0.5), number(values, name, "uncertainty", 0), policy); + criteria, levels, number(context, values, name, "threshold", 0.5), + number(context, values, name, "uncertainty", 0), policy); } private static > T enumeration(Node node, String question, String field, Class type) { @@ -164,14 +165,15 @@ private static > T enumeration(Node node, String question, Str } } - private static double number(Map values, String question, String name, double fallback) { + private static double number( + CamelContext context, Map values, String question, String name, double fallback) { if (!values.containsKey(name)) { return fallback; } Node node = values.get(name); String raw = asText(node); try { - return Double.parseDouble(raw); + return Double.parseDouble(context.resolvePropertyPlaceholders(raw)); } catch (NumberFormatException e) { throw new YamlDeserializationException( node, diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java index e2b3f13a7f24b..6115b5a8c4428 100644 --- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java +++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java @@ -25,8 +25,11 @@ import org.apache.camel.Exchange; import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.dsl.support.RouteBuilderLoaderSupport; +import org.apache.camel.dsl.xml.jaxb.JaxbXmlRoutesBuilderLoader; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.language.semantic.SemanticLanguage; +import org.apache.camel.model.SemanticDefinitionHelper; import org.apache.camel.model.app.SemanticDefinition; import org.apache.camel.spi.Resource; import org.apache.camel.support.DefaultExchange; @@ -113,7 +116,7 @@ private static String document(String root, String contents) { } @ParameterizedTest - @ValueSource(strings = { "java", "camel", "routes" }) + @ValueSource(strings = { "java", "camel", "routes", "jaxb" }) void nativeDeclarationsEvaluateSingleQuestionsAndMixedBatches(String dsl) throws Exception { if (dsl.equals("java")) { context.addRoutes(new RouteBuilder() { @@ -135,7 +138,16 @@ public void configure() { ${exchangeProperty.decision[department]} """; - load("questions.xml", document(dsl, route + xmlQuestions())); + if (dsl.equals("jaxb")) { + try (var loader = new JaxbXmlRoutesBuilderLoader()) { + loader.setCamelContext(context); + loader.start(); + context.addRoutes(loader.loadRoutesBuilder( + ResourceHelper.fromString("questions.xml", document("routes", route + xmlQuestions())))); + } + } else { + load("questions.xml", document(dsl, route + xmlQuestions())); + } } assertThat(states).isEmpty(); SemanticQuestion urgent = SemanticQuestions.get(context).get("urgent"); @@ -233,10 +245,10 @@ void declarationSourceCanBeReplacedAndRemovedIndependentlyOfItsResource(boolean Resource resource = trackResource ? ResourceHelper.fromString("questions.xml", "") : null; SemanticDefinition definition = new SemanticDefinition(); definition.question("first").type("boolean").instructions("Valid?"); - SemanticDefinition.configure(context, resource, "declared-source", definition); + SemanticDefinitionHelper.configure(context, resource, "declared-source", definition); SemanticDefinition replacement = new SemanticDefinition(); replacement.question("second").type("boolean").instructions("Updated?"); - SemanticDefinition.configure(context, resource, "declared-source", replacement); + SemanticDefinitionHelper.configure(context, resource, "declared-source", replacement); SemanticQuestions questions = SemanticQuestions.get(context); assertThatThrownBy(() -> questions.get("first")).hasMessageContaining("Unknown"); assertThat(questions.get("second")).isNotNull(); @@ -295,6 +307,120 @@ public void configure() { assertThat(states).isEmpty(); } + @ParameterizedTest + @ValueSource(strings = { "java", "xml" }) + void numericPlaceholdersResolveBeforeValidation(String dsl) throws Exception { + if (dsl.equals("java")) { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + semanticQuestions().question("urgent").type("boolean").instructions("Urgent?") + .threshold("{{threshold:0.8}}").uncertainty("{{uncertainty:0.1}}"); + } + }); + } else { + load("questions.xml", document("routes", xmlQuestions() + .replace("threshold=\"0.8\"", "threshold=\"{{threshold:0.8}}\"") + .replace("uncertainty=\"0.1\"", "uncertainty=\"{{uncertainty:0.1}}\""))); + } + assertThat(SemanticQuestions.get(context).get("urgent").getThreshold()).isEqualTo(0.8); + assertThat(SemanticQuestions.get(context).get("urgent").getUncertainty()).isEqualTo(0.1); + assertThatThrownBy(() -> load("invalid.xml", document("routes", """ + + Urgent? + + """))) + .hasMessageContaining("Invalid semantic question 'invalid': threshold must be a valid number") + .hasRootCauseInstanceOf(NumberFormatException.class); + } + + @ParameterizedTest + @ValueSource(strings = { "routes", "camel" }) + void repeatedUnchangedPreparsePreservesDeclarationsAndBeans(String root) throws Exception { + var loader = PluginHelper.getRoutesLoader(context).getRoutesLoader("xml"); + Resource resource = ResourceHelper.fromString("questions.xml", document(root, xmlQuestions() + + (root.equals("camel") + ? "" + : ""))); + loader.preParseRoute(resource); + SemanticQuestion question = SemanticQuestions.get(context).get("urgent"); + Object bean = context.getRegistry().lookupByName("counter"); + loader.preParseRoute(resource); + assertThat(SemanticQuestions.get(context).get("urgent")).isSameAs(question); + if (root.equals("camel")) { + assertThat(bean).isNotNull(); + assertThat(context.getRegistry().lookupByName("counter")).isSameAs(bean); + } + } + + @ParameterizedTest + @MethodSource("externalFailures") + void retryAfterExternalFailureReadsChangedXml(String root, boolean failPreparse) throws Exception { + var failingLoader = new RouteBuilderLoaderSupport("fail") { + @Override + public void preParseRoute(Resource resource) { + if (failPreparse) { + throw new IllegalStateException("foreign preparse failure"); + } + } + + @Override + public RouteBuilder doLoadRouteBuilder(Resource resource) { + return new RouteBuilder() { + @Override + public void configure() { + throw new IllegalStateException("earlier builder failure"); + } + }; + } + }; + context.getRegistry().bind("routes-builder-loader-fail", failingLoader); + Path file = directory.resolve("questions.xml"); + Files.writeString(file, document(root, xmlQuestions())); + Resource resource = ResourceHelper.resolveResource(context, file.toUri().toString()); + Resource broken = ResourceHelper.fromString("broken.fail", ""); + var loader = PluginHelper.getRoutesLoader(context); + assertThatThrownBy(() -> loader.loadRoutes(failPreparse + ? List.of(resource, broken) : List.of(broken, resource))) + .hasMessageContaining(failPreparse ? "foreign preparse failure" : "earlier builder failure"); + assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${header.myState}"); + Files.writeString(file, document(root, xmlQuestions().replace("header.myState", "header.updated") + """ + + ref:department + + """)); + loader.loadRoutes(resource); + assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${header.updated}"); + try (var template = context.createProducerTemplate()) { + assertThat(template.requestBodyAndHeader("direct:updated", "old", "updated", "new")).isEqualTo("billing"); + } + assertThat(states).containsExactly("new"); + } + + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void deferredBeansKeepTheirOwningResourceWhenAnotherBuilderFails(boolean removeBean) throws Exception { + var loader = PluginHelper.getRoutesLoader(context); + Resource first = ResourceHelper.fromString("first.xml", document("camel", xmlQuestions())); + Resource second = ResourceHelper.fromString("second.xml", document("camel", """ + + """)); + assertThatThrownBy(() -> loader.loadRoutes(List.of(first, second))).hasMessageContaining("Error creating bean"); + if (removeBean) { + loader.loadRoutes(ResourceHelper.fromString("second.xml", document("camel", ""))); + assertThat(context.getRegistry().lookupByName("deferred")).isNull(); + } else { + context.getRegistry().bind("factory", new DeferredBeanFactory()); + loader.loadRoutes(second); + assertThat(context.getRegistry().lookupByName("deferred")).isEqualTo("created"); + } + } + + static Stream externalFailures() { + return Stream.of(Arguments.of("routes", true), Arguments.of("routes", false), + Arguments.of("camel", true), Arguments.of("camel", false)); + } + @Test void failedResourceBatchDoesNotReuseEarlierCachedDeclarationsOnRetry() throws Exception { String second = "" @@ -378,6 +504,12 @@ private void load(String location, String xml) throws Exception { PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString(location, xml)); } + public static class DeferredBeanFactory { + public static String create() { + return "created"; + } + } + private static class TestWatcher extends RouteWatcherReloadStrategy { void reload(Resource resource) { onRouteReload(List.of(resource), false); diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticQuestionDefinition.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticQuestionDefinition.json index 12d4bbcbe2567..a530faab49795 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticQuestionDefinition.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticQuestionDefinition.json @@ -15,9 +15,9 @@ "name": { "index": 0, "kind": "attribute", "displayName": "Name", "group": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The context-wide question name." }, "type": { "index": 1, "kind": "attribute", "displayName": "Type", "group": "common", "required": true, "type": "enum", "javaType": "java.lang.String", "enum": [ "boolean", "choice", "score" ], "deprecated": false, "autowired": false, "secret": false, "description": "The question type." }, "state": { "index": 2, "kind": "attribute", "displayName": "State", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The Simple expression selecting the message state." }, - "threshold": { "index": 3, "kind": "attribute", "displayName": "Threshold", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The boolean decision threshold." }, - "uncertainty": { "index": 4, "kind": "attribute", "displayName": "Uncertainty", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The boolean uncertainty band." }, - "uncertaintyPolicy": { "index": 5, "kind": "attribute", "displayName": "Uncertainty Policy", "group": "common", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "fail", "non-match" ], "deprecated": false, "autowired": false, "secret": false, "description": "The boolean uncertainty policy." }, + "threshold": { "index": 3, "kind": "attribute", "displayName": "Threshold", "group": "common", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "0.5", "description": "The boolean decision threshold." }, + "uncertainty": { "index": 4, "kind": "attribute", "displayName": "Uncertainty", "group": "common", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "0", "description": "The boolean uncertainty band." }, + "uncertaintyPolicy": { "index": 5, "kind": "attribute", "displayName": "Uncertainty Policy", "group": "common", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "fail", "non-match" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "fail", "description": "The boolean uncertainty policy." }, "instructions": { "index": 6, "kind": "element", "displayName": "Instructions", "group": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Instructions describing the judgment to make." }, "criterion": { "index": 7, "kind": "element", "displayName": "Criterion", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Named choice criteria, or optional true\/false boolean criteria." }, "level": { "index": 8, "kind": "element", "displayName": "Level", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Ordered descriptive levels for a score question." } diff --git a/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java b/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java index 5897665f64f18..5e2e23be00651 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java @@ -46,6 +46,7 @@ import org.apache.camel.model.RouteTemplateDefinition; import org.apache.camel.model.RouteTemplatesDefinition; import org.apache.camel.model.RoutesDefinition; +import org.apache.camel.model.SemanticDefinitionHelper; import org.apache.camel.model.TemplatedRouteDefinition; import org.apache.camel.model.TemplatedRoutesDefinition; import org.apache.camel.model.app.SemanticDefinition; @@ -202,6 +203,8 @@ public String toString() { /** * Declares named semantic questions for use with the semantic language. Requires camel-semantic at runtime. + * + * @since 4.23 */ public SemanticDefinition semanticQuestions() { if (getRouteCollection().getSemantic() == null) { @@ -925,7 +928,7 @@ protected void checkInitialized() throws Exception { String source = getResource() != null ? getResource().getLocation() : "java:" + camelContext.getUuidGenerator().generateUuid(); - SemanticDefinition.configure(camelContext, getResource(), source, getRouteCollection().getSemantic()); + SemanticDefinitionHelper.configure(camelContext, getResource(), source, getRouteCollection().getSemantic()); } // remember the source resource diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java index f1ae6086fb01d..b171aa1646991 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java @@ -83,14 +83,6 @@ public class RoutesDefinition extends OptionalIdentifiedDefinition routes) { this.routes = routes; } + public SemanticDefinition getSemantic() { + return semantic; + } + + public void setSemantic(SemanticDefinition semantic) { + this.semantic = semantic; + } + public List getInterceptFroms() { return interceptFroms; } diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java b/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java new file mode 100644 index 0000000000000..95f4a8b1c9226 --- /dev/null +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java @@ -0,0 +1,48 @@ +/* + * 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.model; + +import org.apache.camel.CamelContext; +import org.apache.camel.model.app.SemanticDefinition; +import org.apache.camel.model.spi.SemanticDefinitionConfigurer; +import org.apache.camel.spi.Resource; + +/** Connects semantic declarations to the optional semantic language module. */ +public final class SemanticDefinitionHelper { + private SemanticDefinitionHelper() { + } + + /** Register declarations before route initialization, without requiring semantic support for ordinary routes. */ + public static void configure( + CamelContext context, Resource resource, String source, SemanticDefinition definition) { + SemanticDefinitionConfigurer configurer + = context.getCamelContextExtension().getContextPlugin(SemanticDefinitionConfigurer.class); + if (configurer == null) { + if (definition == null || definition.getQuestions().isEmpty()) { + return; + } + // Concurrent discovery may create equivalent instances of the stateless default configurer. + // The semantic module synchronizes access to shared question state. + configurer = context.getCamelContextExtension().getDefaultFactoryFinder() + .newInstance("semantic-configurer", SemanticDefinitionConfigurer.class) + .orElseThrow(() -> new IllegalArgumentException( + "Semantic question declarations require camel-semantic on the classpath")); + context.getCamelContextExtension().addContextPlugin(SemanticDefinitionConfigurer.class, configurer); + } + configurer.configure(context, resource, source, definition); + } +} diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java index ac1f2f50c0ddd..ae3fe8a55e8e8 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java @@ -24,9 +24,7 @@ import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlType; -import org.apache.camel.CamelContext; import org.apache.camel.spi.Metadata; -import org.apache.camel.spi.Resource; /** Named, provider-independent semantic question declarations. */ @Metadata(label = "configuration") @@ -52,24 +50,4 @@ public SemanticQuestionDefinition question(String name) { questions.add(question); return question; } - - /** Register declarations before route initialization, without requiring semantic support for ordinary routes. */ - public static void configure( - CamelContext context, Resource resource, String source, SemanticDefinition definition) { - SemanticDefinitionConfigurer configurer - = context.getCamelContextExtension().getContextPlugin(SemanticDefinitionConfigurer.class); - if (configurer == null) { - if (definition == null || definition.getQuestions().isEmpty()) { - return; - } - // Concurrent discovery may create equivalent instances of the stateless default configurer. - // The semantic module synchronizes access to shared question state. - configurer = context.getCamelContextExtension().getDefaultFactoryFinder() - .newInstance("semantic-configurer", SemanticDefinitionConfigurer.class) - .orElseThrow(() -> new IllegalArgumentException( - "Semantic question declarations require camel-semantic on the classpath")); - context.getCamelContextExtension().addContextPlugin(SemanticDefinitionConfigurer.class, configurer); - } - configurer.configure(context, resource, source, definition); - } } diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticQuestionDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticQuestionDefinition.java index 1a5c70c833c38..15f4f6787c653 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticQuestionDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticQuestionDefinition.java @@ -43,13 +43,13 @@ public class SemanticQuestionDefinition { @Metadata(description = "The Simple expression selecting the message state.") private String state; @XmlAttribute - @Metadata(description = "The boolean decision threshold.") + @Metadata(javaType = "java.lang.Double", defaultValue = "0.5", description = "The boolean decision threshold.") private String threshold; @XmlAttribute - @Metadata(description = "The boolean uncertainty band.") + @Metadata(javaType = "java.lang.Double", defaultValue = "0", description = "The boolean uncertainty band.") private String uncertainty; @XmlAttribute - @Metadata(enums = "fail,non-match", description = "The boolean uncertainty policy.") + @Metadata(defaultValue = "fail", enums = "fail,non-match", description = "The boolean uncertainty policy.") private String uncertaintyPolicy; @XmlElement(required = true) @Metadata(required = true, description = "Instructions describing the judgment to make.") @@ -147,13 +147,23 @@ public SemanticQuestionDefinition state(String state) { /** The boolean decision threshold. */ public SemanticQuestionDefinition threshold(double threshold) { - this.threshold = Double.toString(threshold); + return threshold(Double.toString(threshold)); + } + + /** The boolean decision threshold, allowing property placeholders. */ + public SemanticQuestionDefinition threshold(String threshold) { + this.threshold = threshold; return this; } /** The boolean uncertainty band. */ public SemanticQuestionDefinition uncertainty(double uncertainty) { - this.uncertainty = Double.toString(uncertainty); + return uncertainty(Double.toString(uncertainty)); + } + + /** The boolean uncertainty band, allowing property placeholders. */ + public SemanticQuestionDefinition uncertainty(String uncertainty) { + this.uncertainty = uncertainty; return this; } diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinitionConfigurer.java b/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java similarity index 93% rename from core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinitionConfigurer.java rename to core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java index 49977893df9df..06485ca981061 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinitionConfigurer.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java @@ -14,9 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.camel.model.app; +package org.apache.camel.model.spi; import org.apache.camel.CamelContext; +import org.apache.camel.model.app.SemanticDefinition; import org.apache.camel.spi.Resource; /** Installs semantic declarations using the optional semantic language module. */ diff --git a/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java b/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java index 67d18af71fc0b..fa651e1963c87 100644 --- a/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java +++ b/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java @@ -1151,10 +1151,10 @@ protected void doWriteSemanticDefinition(StringBuilder result, SemanticDefinitio result.append(".state(").append(quote(question.getState())).append(")"); } if (question.getThreshold() != null) { - result.append(".threshold(").append(Double.parseDouble(question.getThreshold())).append(")"); + result.append(".threshold(").append(quote(question.getThreshold())).append(")"); } if (question.getUncertainty() != null) { - result.append(".uncertainty(").append(Double.parseDouble(question.getUncertainty())).append(")"); + result.append(".uncertainty(").append(quote(question.getUncertainty())).append(")"); } if (question.getUncertaintyPolicy() != null) { result.append(".uncertaintyPolicy(").append(quote(question.getUncertaintyPolicy())).append(")"); diff --git a/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java b/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java index a8a462e986bb7..ab3fc4fad7b8e 100644 --- a/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java +++ b/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java @@ -139,12 +139,25 @@ void semanticDeclarationsCompileWithTheirRoutes() throws Exception { RoutesDefinition routes = new ModelParser(input, NAMESPACE).parseRoutesDefinition().orElseThrow(); String java = new LwModelToJavaDumper().dumpModelAsJava(null, routes); assertTrue(java.contains("semanticQuestions().question(\"department\")")); - assertTrue(java.contains(".threshold(0.8)")); + assertTrue(java.contains(".threshold(\"0.8\")")); List errors = compile("SemanticRoutes", wrapInRouteBuilder("SemanticRoutes", List.of(java))); assertTrue(errors.isEmpty(), () -> errors + "\n" + java); } } + @Test + void semanticNumericPlaceholdersSurviveJavaExport() throws Exception { + try (var input = Files.newInputStream(XML_IO_RESOURCES.resolve("semantic-routes.xml"))) { + RoutesDefinition routes = new ModelParser(input, NAMESPACE).parseRoutesDefinition().orElseThrow(); + routes.getSemantic().getQuestions().get(0).threshold("{{threshold}}").uncertainty("{{uncertainty}}"); + String java = new LwModelToJavaDumper().dumpModelAsJava(null, routes); + assertTrue(java.contains(".threshold(\"{{threshold}}\")")); + assertTrue(java.contains(".uncertainty(\"{{uncertainty}}\")")); + List errors = compile("SemanticPlaceholders", wrapInRouteBuilder("SemanticPlaceholders", List.of(java))); + assertTrue(errors.isEmpty(), () -> errors + "\n" + java); + } + } + @ParameterizedTest(name = "{0}") @MethodSource("xmlRouteFiles") void xmlToJavaDslCompiles(String xmlFile) throws Exception { diff --git a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java index d6a50e2a08bf8..d976097922d7b 100644 --- a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java +++ b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java @@ -2144,9 +2144,9 @@ protected void doWriteSemanticQuestionDefinition(String name, SemanticQuestionDe doWriteAttribute("name", def.getName(), null); doWriteAttribute("type", def.getType(), null); doWriteAttribute("state", def.getState(), null); - doWriteAttribute("threshold", def.getThreshold(), null); - doWriteAttribute("uncertainty", def.getUncertainty(), null); - doWriteAttribute("uncertaintyPolicy", def.getUncertaintyPolicy(), null); + doWriteAttribute("threshold", def.getThreshold(), "0.5"); + doWriteAttribute("uncertainty", def.getUncertainty(), "0"); + doWriteAttribute("uncertaintyPolicy", def.getUncertaintyPolicy(), "fail"); doWriteElement("instructions", def.getInstructions(), this::doWriteString); doWriteList(null, "criterion", def.getCriteria(), this::doWritePropertyDefinition); doWriteList(null, "level", def.getLevels(), this::doWriteString); diff --git a/dsl/camel-jbang/camel-jbang-mcp/pom.xml b/dsl/camel-jbang/camel-jbang-mcp/pom.xml index 1cff396141364..227616abc2bbb 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/pom.xml +++ b/dsl/camel-jbang/camel-jbang-mcp/pom.xml @@ -116,6 +116,11 @@ camel-java-joor-dsl + + org.apache.camel + camel-semantic + + io.swagger.core.v3 @@ -147,6 +152,11 @@ + + org.apache.camel + camel-xml-io-dsl + test + io.quarkus quarkus-junit diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java index 8666b7206d00c..92cb87bb0fd2a 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java @@ -30,8 +30,12 @@ import io.quarkiverse.mcp.server.ToolArg; import io.quarkiverse.mcp.server.ToolCallException; import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.model.ExpressionNode; +import org.apache.camel.model.ProcessorDefinitionHelper; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.RoutesDefinition; +import org.apache.camel.model.app.SemanticDefinition; +import org.apache.camel.semantic.DefaultSemanticDefinitionConfigurer; import org.apache.camel.spi.Resource; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.ResourceHelper; @@ -154,6 +158,7 @@ private String transformYamlToXml(String yaml) throws Exception { RoutesDefinition rd = new RoutesDefinition(); rd.setRoutes(routeDefs); + rd.setSemantic(DefaultSemanticDefinitionConfigurer.getDefinition(ctx)); StringWriter sw = new StringWriter(); new org.apache.camel.xml.out.ModelWriter(sw).writeRoutesDefinition(rd); @@ -179,9 +184,17 @@ private String transformJavaToFormat(String java, String targetFormat) throws Ex "Could not parse Java route. Ensure it contains a valid route definition."); } + // Java expression clauses are normally materialized when processors are created. + routeDefs.forEach(route -> ProcessorDefinitionHelper.filterTypeInOutputs(route.getOutputs(), ExpressionNode.class) + .forEach(ExpressionNode::preCreateProcessor)); + if ("yaml".equals(targetFormat)) { YamlModelWriter writer = new YamlModelWriter(); List roots = new ArrayList<>(); + SemanticDefinition semantic = DefaultSemanticDefinitionConfigurer.getDefinition(ctx); + if (semantic != null) { + roots.add(writer.writeSemanticDefinition(semantic)); + } for (RouteDefinition route : routeDefs) { roots.add(writer.writeRouteDefinition(route)); } @@ -189,6 +202,7 @@ private String transformJavaToFormat(String java, String targetFormat) throws Ex } else { RoutesDefinition rd = new RoutesDefinition(); rd.setRoutes(routeDefs); + rd.setSemantic(DefaultSemanticDefinitionConfigurer.getDefinition(ctx)); StringWriter sw = new StringWriter(); new org.apache.camel.xml.out.ModelWriter(sw).writeRoutesDefinition(rd); diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java index 54a613b56314d..4c45f5b9eb78d 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java @@ -16,7 +16,13 @@ */ package org.apache.camel.dsl.jbang.core.commands.mcp; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.semantic.SemanticQuestions; +import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.ResourceHelper; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import static org.assertj.core.api.Assertions.assertThat; @@ -59,6 +65,70 @@ void transformXmlToYamlPreservesSemanticDeclarations() { assertThat(result.result).contains("semantic:", "question:", "urgent:", "Urgent?", "ref:urgent"); } + @ParameterizedTest + @CsvSource({ "yaml,xml", "java,xml", "java,yaml" }) + void semanticDeclarationsSurviveConversionAndReload(String source, String target) throws Exception { + String yaml = """ + - semantic: + question: + urgent: + type: boolean + instructions: Urgent? + threshold: 0.8 + uncertainty: 0.1 + uncertaintyPolicy: non-match + department: + type: choice + state: ${header.selected} + instructions: Which department? + criteria: + billing: Invoices + technical: Bugs + priority: + type: score + instructions: Priority? + criteria: [Low, High] + - route: + from: + uri: direct:input + steps: + - setBody: + expression: + language: + language: semantic + expression: ref:department + """; + String java = """ + import org.apache.camel.builder.RouteBuilder; + public class SemanticRoute extends RouteBuilder { + public void configure() { + semanticQuestions().question("urgent").type("boolean").instructions("Urgent?") + .threshold(0.8).uncertainty(0.1).uncertaintyPolicy("non-match"); + semanticQuestions().question("department").type("choice").instructions("Which department?") + .state("${header.selected}").criterion("billing", "Invoices").criterion("technical", "Bugs"); + semanticQuestions().question("priority").type("score").instructions("Priority?") + .level("Low").level("High"); + from("direct:input").setBody().language("semantic", "ref:department"); + } + } + """; + var result = createTools().camel_transform_route(source.equals("java") ? java : yaml, source, target); + assertThat(result.supported).isTrue(); + assertThat(result.result).contains("ref:department"); + try (var context = new DefaultCamelContext()) { + context.build(); + PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("result." + target, result.result)); + var questions = SemanticQuestions.get(context); + assertThat(questions.get("urgent").getThreshold()).isEqualTo(0.8); + assertThat(questions.get("urgent").getUncertainty()).isEqualTo(0.1); + assertThat(questions.get("urgent").getUncertaintyPolicy().name()).isEqualTo("NON_MATCH"); + assertThat(questions.get("department").getCriteria()).containsEntry("billing", "Invoices") + .containsEntry("technical", "Bugs"); + assertThat(questions.get("department").getState()).isEqualTo("${header.selected}"); + assertThat(questions.get("priority").getLevels()).containsExactly("Low", "High"); + } + } + @Test void transformYamlToXml() { TransformTools tools = createTools(); diff --git a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java index a945d754496fd..2319f7c356484 100644 --- a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java +++ b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -43,6 +44,7 @@ import org.apache.camel.model.RouteTemplateDefinition; import org.apache.camel.model.RouteTemplatesDefinition; import org.apache.camel.model.RoutesDefinition; +import org.apache.camel.model.SemanticDefinitionHelper; import org.apache.camel.model.TemplatedRouteDefinition; import org.apache.camel.model.TemplatedRoutesDefinition; import org.apache.camel.model.app.BeansDefinition; @@ -94,16 +96,25 @@ public XmlRoutesBuilderLoader() { public void preParseRoute(Resource resource) throws Exception { // preparsing is done at early stage, so we have a chance to load additional beans and populate // Camel registry - if (preparseDone.getOrDefault(resource.getLocation(), false)) { - return; - } try { - XmlStreamInfo xmlInfo = xmlInfo(resource); + // Main may preparse twice. Reuse unchanged input, but refresh edited resources after a failed batch. + Resource snapshot = new CachedResource(resource); + Resource previous = resourceCache.get(resource.getLocation()); + if (preparseDone.getOrDefault(resource.getLocation(), false) && previous != null) { + try (var current = snapshot.getInputStream(); var cached = previous.getInputStream()) { + if (Arrays.equals(current.readAllBytes(), cached.readAllBytes())) { + return; + } + } + } + clearCaches(resource.getLocation()); + resourceCache.put(resource.getLocation(), snapshot); + XmlStreamInfo xmlInfo = xmlInfo(snapshot); if (xmlInfo.isValid()) { String root = xmlInfo.getRootElementName(); SemanticDefinition semantic = null; if ("beans".equals(root) || "blueprint".equals(root) || "camel".equals(root)) { - new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) + new XmlModelParser(snapshot, xmlInfo.getRootElementNamespace()) .parseBeansDefinition() .ifPresent(bd -> { registerBeans(resource, bd); @@ -114,14 +125,14 @@ public void preParseRoute(Resource resource) throws Exception { semantic = app.getSemantic(); } } else if ("routes".equals(root) || "route".equals(root)) { - RoutesDefinition routes = new XmlModelParser(resource(resource), xmlInfo.getRootElementNamespace()) + RoutesDefinition routes = new XmlModelParser(snapshot, xmlInfo.getRootElementNamespace()) .parseRoutesDefinition().orElse(null); if (routes != null) { routesCache.put(resource.getLocation(), routes); semantic = routes.getSemantic(); } } - SemanticDefinition.configure(getCamelContext(), resource, resource.getLocation(), semantic); + SemanticDefinitionHelper.configure(getCamelContext(), resource, resource.getLocation(), semantic); } preparseDone.put(resource.getLocation(), true); } catch (Exception e) { @@ -131,6 +142,7 @@ public void preParseRoute(Resource resource) throws Exception { camelAppCache.clear(); routesCache.clear(); preparseDone.clear(); + delayedRegistrations.clear(); throw e; } } @@ -141,6 +153,7 @@ private void clearCaches(String location) { camelAppCache.remove(location); routesCache.remove(location); preparseDone.remove(location); + delayedRegistrations.removeIf(def -> def.getResource() != null && location.equals(def.getResource().getLocation())); } @Override @@ -237,7 +250,6 @@ private void configureCamel(BeansDefinition app) { if (!delayedRegistrations.isEmpty()) { // some of the beans were not available yet, so we have to try register them now for (BeanFactoryDefinition def : delayedRegistrations) { - def.setResource(getResource()); registerBeanDefinition(def, false); } delayedRegistrations.clear(); diff --git a/dsl/camel-xml-jaxb-dsl/src/main/java/org/apache/camel/dsl/xml/jaxb/JaxbXmlRoutesBuilderLoader.java b/dsl/camel-xml-jaxb-dsl/src/main/java/org/apache/camel/dsl/xml/jaxb/JaxbXmlRoutesBuilderLoader.java index 639a8ff0ed821..b27786fa42208 100644 --- a/dsl/camel-xml-jaxb-dsl/src/main/java/org/apache/camel/dsl/xml/jaxb/JaxbXmlRoutesBuilderLoader.java +++ b/dsl/camel-xml-jaxb-dsl/src/main/java/org/apache/camel/dsl/xml/jaxb/JaxbXmlRoutesBuilderLoader.java @@ -84,6 +84,7 @@ public void configure() throws Exception { try (InputStream is = resourceInputStream(resource)) { RoutesDefinition routes = loadRoutesDefinition(getCamelContext(), is); if (routes != null) { + getRouteCollection().setSemantic(routes.getSemantic()); // xml routes must be prepared in the same way java-dsl (via RoutesDefinition) // so create a copy and use the fluent builder to add the route for (RouteDefinition route : routes.getRoutes()) { diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java index 0a4414f552324..71974099b4d50 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java @@ -341,6 +341,21 @@ void schemaAcceptsBooleanAndScoreDefinitionsAndRejectsWrongCriteria() throws Exc .hasStackTraceContaining("Node type map is invalid, expected array"); } + @Test + void numericPlaceholdersResolveBeforeValidation() throws Exception { + loadRoutesNoValidate(""" + - semantic: + question: + urgent: + type: boolean + instructions: Urgent? + threshold: "{{threshold:0.8}}" + uncertainty: "{{uncertainty:0.1}}" + """); + assertThat(SemanticQuestions.get(context).get("urgent").getThreshold()).isEqualTo(0.8); + assertThat(SemanticQuestions.get(context).get("urgent").getUncertainty()).isEqualTo(0.1); + } + @ParameterizedTest @ValueSource(strings = { "threshold", "uncertainty" }) void invalidNumericValuesIdentifyQuestionFieldAndLocation(String field) { From 0bb166a78dfe9539e9dd3c1194a668d7630acfa4 Mon Sep 17 00:00:00 2001 From: Luigi De Masi Date: Wed, 30 Sep 2026 08:47:53 +0200 Subject: [PATCH 06/11] CAMEL-25138: Refine semantic declaration export Export registered questions through the model configurer SPI, including lazy discovery for YAML-only declarations, and omit default boolean decision policies from converted output. Cover Java filter and nested Simple expression conversion to YAML/XML, default-policy round trips, and numeric placeholders. Include the underlying error when conversion fails and document temporary-context property resolution. Validation: 549 tests passed on Java 17 across core-model, semantic and MCP. Full repository clean install with tests skipped passed on Java 21 across all 695 modules. Regenerated catalog documentation is included. Co-authored-by: Codex Signed-off-by: Luigi De Masi --- .../camel/catalog/docs/semantic-language.adoc | 11 ++- .../src/main/docs/semantic-language.adoc | 11 ++- .../DefaultSemanticDefinitionConfigurer.java | 15 ++- .../camel/model/SemanticDefinitionHelper.java | 29 +++++- .../spi/SemanticDefinitionConfigurer.java | 5 + .../core/commands/mcp/TransformTools.java | 16 +++- .../core/commands/mcp/TransformToolsTest.java | 95 ++++++++++++++++++- 7 files changed, 160 insertions(+), 22 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 06e37072ac63b..c1a09c1859366 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 @@ -184,8 +184,15 @@ can also be selected together using `refs:name1,name2`, as described below. Direct exports of a `RoutesDefinition` containing semantic declarations preserve them. The MCP route transformation tool also preserves declarations in XML-to-YAML, YAML-to-XML, and -Java-to-YAML/XML conversions. Conversions that load a Camel context export the registered -questions with their effective numeric policies. Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only +Java-to-YAML/XML conversions. YAML-to-XML and Java-to-YAML/XML conversions load a temporary +Camel context and export the registered questions with their effective numeric policies, +omitting settings that match the defaults. Numeric placeholders must resolve in that context; +the tool does not load the application's external properties. Provide a placeholder default, +such as `{{semantic.threshold:0.5}}`, or a concrete value when the property is unavailable. +Unresolved required properties fail conversion. XML-to-YAML conversion preserves placeholder +text without resolving it. + +Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only the routes and omit the question declarations. Supply the declarations separately before reloading those dumps. 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 06e37072ac63b..c1a09c1859366 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 @@ -184,8 +184,15 @@ can also be selected together using `refs:name1,name2`, as described below. Direct exports of a `RoutesDefinition` containing semantic declarations preserve them. The MCP route transformation tool also preserves declarations in XML-to-YAML, YAML-to-XML, and -Java-to-YAML/XML conversions. Conversions that load a Camel context export the registered -questions with their effective numeric policies. Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only +Java-to-YAML/XML conversions. YAML-to-XML and Java-to-YAML/XML conversions load a temporary +Camel context and export the registered questions with their effective numeric policies, +omitting settings that match the defaults. Numeric placeholders must resolve in that context; +the tool does not load the application's external properties. Provide a placeholder default, +such as `{{semantic.threshold:0.5}}`, or a concrete value when the property is unavailable. +Unresolved required properties fail conversion. XML-to-YAML conversion preserves placeholder +text without resolving it. + +Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only the routes and omit the question declarations. Supply the declarations separately before reloading those dumps. diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java index 9971fe18596d2..909454e17faf7 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java @@ -53,8 +53,8 @@ public void configure(CamelContext context, Resource resource, String source, Se SemanticQuestions.get(context).replace("model:" + source, resource, questions); } - /** Export a snapshot of the registered questions and their effective decision policies. */ - public static SemanticDefinition getDefinition(CamelContext context) { + @Override + public SemanticDefinition getDefinition(CamelContext context) { SemanticQuestions questions = context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class); if (questions == null) { return null; @@ -68,8 +68,15 @@ public static SemanticDefinition getDefinition(CamelContext context) { question.getCriteria().forEach(target::criterion); question.getLevels().forEach(target::level); if (question.getType() == SemanticQuestion.Type.BOOLEAN) { - target.threshold(question.getThreshold()).uncertainty(question.getUncertainty()) - .uncertaintyPolicy(question.getUncertaintyPolicy().name().toLowerCase(Locale.ROOT).replace('_', '-')); + if (question.getThreshold() != 0.5) { + target.threshold(question.getThreshold()); + } + if (question.getUncertainty() != 0) { + target.uncertainty(question.getUncertainty()); + } + if (question.getUncertaintyPolicy() != SemanticQuestion.UncertaintyPolicy.FAIL) { + target.uncertaintyPolicy(question.getUncertaintyPolicy().name().toLowerCase(Locale.ROOT).replace('_', '-')); + } } }); return definition; diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java b/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java index 95f4a8b1c9226..57ab5253c1e27 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java @@ -35,14 +35,33 @@ public static void configure( if (definition == null || definition.getQuestions().isEmpty()) { return; } + configurer = getConfigurer(context); + if (configurer == null) { + throw new IllegalArgumentException("Semantic question declarations require camel-semantic on the classpath"); + } + } + configurer.configure(context, resource, source, definition); + } + + /** Export registered questions without requiring the semantic module for ordinary routes. */ + public static SemanticDefinition getDefinition(CamelContext context) { + // YAML declarations can populate the registry before the configurer has been discovered. + SemanticDefinitionConfigurer configurer = getConfigurer(context); + return configurer != null ? configurer.getDefinition(context) : null; + } + + private static SemanticDefinitionConfigurer getConfigurer(CamelContext context) { + SemanticDefinitionConfigurer configurer + = context.getCamelContextExtension().getContextPlugin(SemanticDefinitionConfigurer.class); + if (configurer == null) { // Concurrent discovery may create equivalent instances of the stateless default configurer. // The semantic module synchronizes access to shared question state. configurer = context.getCamelContextExtension().getDefaultFactoryFinder() - .newInstance("semantic-configurer", SemanticDefinitionConfigurer.class) - .orElseThrow(() -> new IllegalArgumentException( - "Semantic question declarations require camel-semantic on the classpath")); - context.getCamelContextExtension().addContextPlugin(SemanticDefinitionConfigurer.class, configurer); + .newInstance("semantic-configurer", SemanticDefinitionConfigurer.class).orElse(null); + if (configurer != null) { + context.getCamelContextExtension().addContextPlugin(SemanticDefinitionConfigurer.class, configurer); + } } - configurer.configure(context, resource, source, definition); + return configurer; } } diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java b/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java index 06485ca981061..38db8263f6b57 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java @@ -27,4 +27,9 @@ public interface SemanticDefinitionConfigurer { * the owner independently of the optional resource used to track deletion. */ void configure(CamelContext context, Resource resource, String source, SemanticDefinition definition); + + /** Export registered questions with their effective policies, or {@code null} if no definitions are available. */ + default SemanticDefinition getDefinition(CamelContext context) { + return null; + } } diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java index 92cb87bb0fd2a..db658a0f65a31 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java @@ -34,11 +34,12 @@ import org.apache.camel.model.ProcessorDefinitionHelper; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.RoutesDefinition; +import org.apache.camel.model.SemanticDefinitionHelper; import org.apache.camel.model.app.SemanticDefinition; -import org.apache.camel.semantic.DefaultSemanticDefinitionConfigurer; import org.apache.camel.spi.Resource; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.ResourceHelper; +import org.apache.camel.util.ObjectHelper; import org.apache.camel.util.json.JsonObject; import org.apache.camel.xml.in.ModelParser; import org.apache.camel.yaml.out.YamlModelWriter; @@ -100,8 +101,13 @@ public TransformResult camel_transform_route( result.note = "Unsupported transformation: " + fromFormat + " to " + toFormat; } } catch (Throwable e) { + Throwable cause = ObjectHelper.createExceptionIterator(e).next(); + String message = e.getMessage(); + if (cause != e) { + message += ": " + cause.getMessage(); + } throw new ToolCallException( - "Failed to transform route (" + e.getClass().getName() + "): " + e.getMessage(), null); + "Failed to transform route (" + e.getClass().getName() + "): " + message, null); } return result; @@ -158,7 +164,7 @@ private String transformYamlToXml(String yaml) throws Exception { RoutesDefinition rd = new RoutesDefinition(); rd.setRoutes(routeDefs); - rd.setSemantic(DefaultSemanticDefinitionConfigurer.getDefinition(ctx)); + rd.setSemantic(SemanticDefinitionHelper.getDefinition(ctx)); StringWriter sw = new StringWriter(); new org.apache.camel.xml.out.ModelWriter(sw).writeRoutesDefinition(rd); @@ -191,7 +197,7 @@ private String transformJavaToFormat(String java, String targetFormat) throws Ex if ("yaml".equals(targetFormat)) { YamlModelWriter writer = new YamlModelWriter(); List roots = new ArrayList<>(); - SemanticDefinition semantic = DefaultSemanticDefinitionConfigurer.getDefinition(ctx); + SemanticDefinition semantic = SemanticDefinitionHelper.getDefinition(ctx); if (semantic != null) { roots.add(writer.writeSemanticDefinition(semantic)); } @@ -202,7 +208,7 @@ private String transformJavaToFormat(String java, String targetFormat) throws Ex } else { RoutesDefinition rd = new RoutesDefinition(); rd.setRoutes(routeDefs); - rd.setSemantic(DefaultSemanticDefinitionConfigurer.getDefinition(ctx)); + rd.setSemantic(SemanticDefinitionHelper.getDefinition(ctx)); StringWriter sw = new StringWriter(); new org.apache.camel.xml.out.ModelWriter(sw).writeRoutesDefinition(rd); diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java index 4c45f5b9eb78d..36170287d7200 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java @@ -16,15 +16,20 @@ */ package org.apache.camel.dsl.jbang.core.commands.mcp; +import io.quarkiverse.mcp.server.ToolCallException; import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.model.FilterDefinition; +import org.apache.camel.model.SetBodyDefinition; import org.apache.camel.semantic.SemanticQuestions; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.ResourceHelper; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class TransformToolsTest { @@ -74,8 +79,8 @@ void semanticDeclarationsSurviveConversionAndReload(String source, String target urgent: type: boolean instructions: Urgent? - threshold: 0.8 - uncertainty: 0.1 + threshold: "{{semantic.export.threshold:0.8}}" + uncertainty: "{{semantic.export.uncertainty:0.1}}" uncertaintyPolicy: non-match department: type: choice @@ -103,7 +108,8 @@ void semanticDeclarationsSurviveConversionAndReload(String source, String target public class SemanticRoute extends RouteBuilder { public void configure() { semanticQuestions().question("urgent").type("boolean").instructions("Urgent?") - .threshold(0.8).uncertainty(0.1).uncertaintyPolicy("non-match"); + .threshold("{{semantic.export.threshold:0.8}}") + .uncertainty("{{semantic.export.uncertainty:0.1}}").uncertaintyPolicy("non-match"); semanticQuestions().question("department").type("choice").instructions("Which department?") .state("${header.selected}").criterion("billing", "Invoices").criterion("technical", "Bugs"); semanticQuestions().question("priority").type("score").instructions("Priority?") @@ -114,7 +120,7 @@ public void configure() { """; var result = createTools().camel_transform_route(source.equals("java") ? java : yaml, source, target); assertThat(result.supported).isTrue(); - assertThat(result.result).contains("ref:department"); + assertThat(result.result).contains("ref:department").doesNotContain("{{semantic.export."); try (var context = new DefaultCamelContext()) { context.build(); PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("result." + target, result.result)); @@ -129,6 +135,87 @@ public void configure() { } } + @ParameterizedTest + @CsvSource({ "yaml,xml", "java,xml", "java,yaml" }) + void semanticConversionOmitsDefaultPolicies(String source, String target) throws Exception { + var result = createTools().camel_transform_route(semanticRoute(source, "0.5"), source, target); + + assertThat(result.supported).isTrue(); + assertThat(result.result).contains("urgent", "Urgent?").doesNotContain("threshold", "uncertainty"); + try (var context = new DefaultCamelContext()) { + context.build(); + PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("result." + target, result.result)); + var question = SemanticQuestions.get(context).get("urgent"); + assertThat(question.getThreshold()).isEqualTo(0.5); + assertThat(question.getUncertainty()).isZero(); + assertThat(question.getUncertaintyPolicy().name()).isEqualTo("FAIL"); + } + } + + @ParameterizedTest + @CsvSource({ "yaml,xml", "java,xml", "java,yaml" }) + void semanticConversionReportsMissingNumericProperty(String source, String target) { + String route = semanticRoute(source, "{{semantic.export.missing.threshold}}"); + + assertThatThrownBy(() -> createTools().camel_transform_route(route, source, target)) + .isInstanceOf(ToolCallException.class) + .hasMessageContaining("Property with key [semantic.export.missing.threshold] not found"); + } + + private static String semanticRoute(String source, String threshold) { + if (source.equals("java")) { + return """ + semanticQuestions().question("urgent").type("boolean").instructions("Urgent?") + .threshold("%s").uncertainty(0).uncertaintyPolicy("fail"); + from("direct:input").setBody().language("semantic", "ref:urgent"); + """.formatted(threshold); + } + return """ + - semantic: + question: + urgent: + type: boolean + instructions: Urgent? + threshold: "%s" + uncertainty: 0 + uncertaintyPolicy: fail + - route: + from: + uri: direct:input + steps: + - setBody: + expression: + language: + language: semantic + expression: ref:urgent + """.formatted(threshold); + } + + @ParameterizedTest + @ValueSource(strings = { "yaml", "xml" }) + void nonSemanticExpressionClausesSurviveConversionAndReload(String target) throws Exception { + String route = """ + from("direct:input") + .filter().simple("${body} == 'hello'") + .setBody().simple("${body.toUpperCase()}") + .end(); + """; + var result = createTools().camel_transform_route(route, "java", target); + + assertThat(result.supported).isTrue(); + assertThat(result.result).doesNotContain("semantic"); + try (var context = new DefaultCamelContext()) { + context.build(); + PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("result." + target, result.result)); + var filter = (FilterDefinition) context.getRouteDefinitions().get(0).getOutputs().get(0); + assertThat(filter.getExpression().getLanguage()).isEqualTo("simple"); + assertThat(filter.getExpression().getExpression()).isEqualTo("${body} == 'hello'"); + var setBody = (SetBodyDefinition) filter.getOutputs().get(0); + assertThat(setBody.getExpression().getLanguage()).isEqualTo("simple"); + assertThat(setBody.getExpression().getExpression()).isEqualTo("${body.toUpperCase()}"); + } + } + @Test void transformYamlToXml() { TransformTools tools = createTools(); From 4e03971ee3ecbcf65621a760d660dcdc656ba5bb Mon Sep 17 00:00:00 2001 From: Luigi De Masi Date: Wed, 30 Sep 2026 09:47:41 +0200 Subject: [PATCH 07/11] CAMEL-25138: Keep semantic declarations in camel-semantic Use a fluent helper in ordinary Java RouteBuilder classes and an optional semantic.xml loader for standalone declarations or declarations alongside routes. Reuse existing builder lifecycle and XML parsing hooks, removing the semantic model, SPI, writers and schema changes from core. Preserve atomic validation and source ownership during reload. Reject lossy generic conversions and document the declaration/export boundary. Co-authored-by: Codex Signed-off-by: Luigi De Masi --- .../camel/catalog/docs/semantic-language.adoc | 136 +++++---- .../camel/catalog/models-app.properties | 2 - .../camel/catalog/models-app/beans.json | 17 +- .../camel/catalog/models-app/camel.json | 17 +- .../models-app/semanticDefinition.json | 17 -- .../semanticQuestionDefinition.json | 25 -- .../apache/camel/catalog/models/routes.json | 3 +- .../camel/catalog/schemas/camel-spring.xsd | 36 --- .../camel/catalog/schemas/camel-xml-io.xsd | 36 --- .../catalog/DocExamplesXmlSchemaTest.java | 6 +- components/camel-ai/camel-semantic/pom.xml | 15 +- .../apache/camel/routes-loader/semantic-xml | 2 + .../src/main/docs/semantic-language.adoc | 136 +++++---- .../DefaultSemanticDefinitionConfigurer.java | 129 --------- .../semantic/SemanticQuestionBuilder.java | 149 ++++++++++ .../camel/semantic/SemanticQuestions.java | 17 +- .../semantic/SemanticQuestionsBuilder.java | 113 ++++++++ .../SemanticXmlRoutesBuilderLoader.java | 216 ++++++++++++++ .../org/apache/camel/semantic-configurer | 1 - .../semantic/SemanticDeclarationDslTest.java | 270 ++++++++---------- .../org/apache/camel/model/app/beans.json | 17 +- .../org/apache/camel/model/app/camel.json | 17 +- .../camel/model/app/semanticDefinition.json | 17 -- .../model/app/semanticQuestionDefinition.json | 25 -- .../org/apache/camel/model/routes.json | 3 +- .../org/apache/camel/model/app/jaxb.index | 2 - .../apache/camel/builder/RouteBuilder.java | 21 -- .../apache/camel/model/RoutesDefinition.java | 16 -- .../camel/model/SemanticDefinitionHelper.java | 67 ----- .../camel/model/app/BeansDefinition.java | 13 - .../camel/model/app/SemanticDefinition.java | 53 ---- .../model/app/SemanticQuestionDefinition.java | 193 ------------- .../spi/SemanticDefinitionConfigurer.java | 35 --- .../camel/java/out/JavaDslModelWriter.java | 2 - .../camel/java/LwModelToJavaDumper.java | 3 - .../java/out/JavaDslModelWriterSupport.java | 39 --- .../camel/java/out/JavaDslCompileTest.java | 26 -- .../org/apache/camel/xml/in/ModelParser.java | 24 -- .../org/apache/camel/xml/out/ModelWriter.java | 20 -- .../apache/camel/xml/out/ModelWriterTest.java | 28 -- .../src/test/resources/beans-semantic.xml | 39 --- .../src/test/resources/semantic-routes.xml | 43 --- .../camel/yaml/out/YamlModelWriter.java | 2 - .../camel/yaml/LwModelToYAMLDumper.java | 3 - .../yaml/out/YamlModelWriterSupport.java | 40 --- .../yaml/out/YamlWriterEdgeCasesTest.java | 33 --- .../core/commands/mcp/TransformTools.java | 24 +- .../core/commands/mcp/TransformToolsTest.java | 109 ++----- .../dsl/xml/io/XmlRoutesBuilderLoader.java | 96 ++----- .../xml/io/XmlRoutesBuilderLoaderTest.java | 20 -- .../xml/jaxb/JaxbXmlRoutesBuilderLoader.java | 1 - .../camel-yaml-dsl-deserializers/pom.xml | 2 - dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml | 5 - .../camel/dsl/yaml/SemanticQuestionTest.java | 33 +-- .../velocity/model-java-dsl-writer.vm | 4 +- .../resources/velocity/model-yaml-writer.vm | 4 +- 56 files changed, 878 insertions(+), 1544 deletions(-) delete mode 100644 catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticDefinition.json delete mode 100644 catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticQuestionDefinition.json create mode 100644 components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/routes-loader/semantic-xml delete mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java create mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java create mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java create mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java delete mode 100644 components/camel-ai/camel-semantic/src/main/resources/META-INF/services/org/apache/camel/semantic-configurer delete mode 100644 core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticDefinition.json delete mode 100644 core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticQuestionDefinition.json delete mode 100644 core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java delete mode 100644 core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java delete mode 100644 core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticQuestionDefinition.java delete mode 100644 core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java delete mode 100644 core/camel-xml-io/src/test/resources/beans-semantic.xml delete mode 100644 core/camel-xml-io/src/test/resources/semantic-routes.xml 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 c1a09c1859366..cc9d87d92481b 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 @@ -100,19 +100,23 @@ Loading declarations does not perform inference. === Java declarations -Use `semanticQuestions().question(name)` in a `RouteBuilder` to declare questions alongside routes: +Use the fluent helper from `camel-semantic` inside an ordinary `RouteBuilder`: [source,java] ---- +import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions; + @Override public void configure() { - semanticQuestions().question("department") - .type("choice") - .state("${header.myState}") - .instructions("Which department should handle this message?") - .criterion("billing", "Invoices, payments, and refunds") - .criterion("technical", "Bugs, outages, and technical problems") - .criterion("other", "Everything else"); + semanticQuestions(this) + .question("department") + .type("choice") + .state("${header.myState}") + .instructions("Which department should handle this message?") + .criterion("billing", "Invoices, payments, and refunds") + .criterion("technical", "Bugs, outages, and technical problems") + .criterion("other", "Everything else") + .register(); from("direct:tickets") .setProperty("department").language("semantic", "ref:department") @@ -120,28 +124,34 @@ public void configure() { } ---- -Declarations are installed after `configure()` completes and before the routes are initialized. -They can appear before or after the routes that reference them within the same builder. -When adding builders to a running context, add the builder declaring shared questions before -builders whose routes reference them. Use `.type("boolean")` with -optional `.threshold(0.8)`, `.uncertainty(0.1)` and `.uncertaintyPolicy("non-match")` for boolean -questions. Use `.type("score")` and successive `.level("description")` calls for ordered score -levels. Java applications can also continue to register immutable `SemanticQuestion` definitions -using `SemanticQuestions.get(context).replace(source, questions)`. +Call `.end().question("anotherName")` to add another question to the group, then `.register()` +once to validate and install the entire group atomically. Register before creating expressions +that refer to these names. When adding builders to a running context, add the builder declaring +shared questions before builders whose routes reference them. + +Use `.type("boolean")` with optional `.threshold(0.8)`, `.uncertainty(0.1)` and +`.uncertaintyPolicy("non-match")` for boolean questions. Use `.type("score")` and successive +`.level("description")` calls for ordered score levels. Applications can also register immutable +`SemanticQuestion` definitions using `SemanticQuestions.get(context).replace(source, questions)`. -Java/XML declarations reserve source keys beginning with `model:`. For a route resource, the -key is `"model:" + resource.getLocation()`; passing this key and an empty map to `replace` -removes that resource's questions. Reloading the resource installs them again. Embedded Java -builders without a resource receive distinct generated source keys. +Each Java resource owns one group. Register all of its questions together; registering again +replaces that resource's previous group. Reloading a Java resource without the helper removes +its declarations. Embedded builders without a resource receive distinct generated source keys. +The helper reserves `"java:" + resource.getLocation()` as the resource source key; passing that +key and an empty map to `replace` removes the resource's questions. === XML declarations -With xref:others:java-xml-io-dsl.adoc[XML DSL], place a `semantic` element inside the `camel` or -`routes` document, before the routes to comply with the XML schema: +The optional XML extension belongs to `camel-semantic`. Include `camel-semantic` and +xref:others:java-xml-io-dsl.adoc[XML DSL] (`camel-xml-io-dsl`), and give declaration files the +extension `*.semantic.xml`. This selects the component's loader without changing the standard +XML loader or Camel core model. + +Both layouts are supported. To keep declarations alongside routes, use `tickets.semantic.xml`: [source,xml] ---- - + Which department should handle this message? @@ -150,51 +160,67 @@ With xref:others:java-xml-io-dsl.adoc[XML DSL], place a `semantic` element insid - + ref:department - + ---- -For boolean questions, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional question -attributes. For score questions, replace the named `criterion` elements with ordered -`level` elements, such as `RoutineUrgentCritical`. -XML declarations are loaded before route references are initialized, including those in another -resource loaded together. Reload replaces the source's questions; -removing its declarations or deleting the resource removes obsolete definitions. +To share declarations across route files, use a standalone `questions.semantic.xml`: + +[source,xml] +---- + + + Which department should handle this message? + + + + + +---- -The JAXB XML loader also supports declarations inside a `routes` document using the -`http://camel.apache.org/schema/spring` namespace. With JAXB, load a separate declaration -resource before the routes that reference it. +Load this together with ordinary `*.xml`, Java or YAML route resources that use `ref:department`. +For example, with Camel Main, set +`camel.main.routes-include-pattern=classpath:questions.semantic.xml,classpath:routes.xml`. +Declarations are registered before consuming routes are configured. Reload replaces the source's +questions; removing the `semantic` block, using an empty block, or deleting the resource removes +obsolete definitions. XML source keys are the resource locations. + +The combined format supports a `routes` root with one optional `semantic` block and ordinary +`route` elements. The standalone format uses a `semantic` root. Namespace-free documents are +supported, as are documents consistently using `http://camel.apache.org/schema/semantic`, +`http://camel.apache.org/schema/xml-io`, or `http://camel.apache.org/schema/spring`. +These extensions do not validate against Camel's standard core XSDs. Ordinary `*.xml` files +and the JAXB loader do not accept embedded semantic declarations. + +For boolean questions, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional question +attributes. For score questions, replace the named `criterion` elements with ordered `level` +elements, such as `RoutineUrgentCritical`. The numeric `threshold` and `uncertainty` options accept property placeholders in all three DSLs. For example, Java accepts `.threshold("{{semantic.threshold:0.5}}")`, and XML accepts `threshold="{{semantic.threshold:0.5}}"`. Values are resolved and validated when declarations -are registered; Java model exports preserve the placeholder text. - -Both Java and XML declarations require `camel-semantic` and use the same context-wide registry, -validation, defaults and adapters as YAML. They do not require `camel-yaml-dsl`. Their questions -can also be selected together using `refs:name1,name2`, as described below. - -=== Exporting declarations - -Direct exports of a `RoutesDefinition` containing semantic declarations preserve them. The MCP -route transformation tool also preserves declarations in XML-to-YAML, YAML-to-XML, and -Java-to-YAML/XML conversions. YAML-to-XML and Java-to-YAML/XML conversions load a temporary -Camel context and export the registered questions with their effective numeric policies, -omitting settings that match the defaults. Numeric placeholders must resolve in that context; -the tool does not load the application's external properties. Provide a placeholder default, -such as `{{semantic.threshold:0.5}}`, or a concrete value when the property is unavailable. -Unresolved required properties fail conversion. XML-to-YAML conversion preserves placeholder -text without resolving it. - -Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only -the routes and omit the question declarations. Supply the declarations separately before -reloading those dumps. +are registered. + +Java and XML declarations use the same context-wide registry, validation, defaults and adapters +as YAML. They do not require `camel-yaml-dsl`. Their questions can also be selected together +using `refs:name1,name2`, as described below. + +=== Exporting routes + +Question declarations live outside Camel's core route model. Generic model exports and runtime +route dumps such as `camel.main.dumpRoutes=yaml` contain only routes; keep declarations separately +and load them before reloading an exported route. + +The MCP route conversion tool rejects Java or YAML inputs containing semantic declarations +because a generic route export would lose those declarations. Convert the routes separately. +Extended XML documents must also be separated into declarations and ordinary routes before +using the generic converter. === Selected state diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app.properties b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app.properties index caf3b24324217..57c9cb5a106f0 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app.properties +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app.properties @@ -4,6 +4,4 @@ constructor constructors properties property -semanticDefinition -semanticQuestionDefinition sslContextParameters diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/beans.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/beans.json index eb9b74f87e8ef..f62e00a438a60 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/beans.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/beans.json @@ -14,14 +14,13 @@ "properties": { "component-scan": { "index": 0, "kind": "element", "displayName": "Component-scan", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Component scanning that can auto-discover Camel route builders from the classpath." }, "bean": { "index": 1, "kind": "element", "displayName": "Bean", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "List of bean definitions to be registered in the Camel registry." }, - "semantic": { "index": 2, "kind": "element", "displayName": "Semantic", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.app.SemanticDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic question declarations." }, - "sslContextParameters": { "index": 3, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, - "dataFormat": { "index": 4, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, - "restConfiguration": { "index": 5, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, - "rest": { "index": 6, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, - "routeConfiguration": { "index": 7, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, - "routeTemplate": { "index": 8, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, - "templatedRoute": { "index": 9, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, - "route": { "index": 10, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } + "sslContextParameters": { "index": 2, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, + "dataFormat": { "index": 3, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, + "restConfiguration": { "index": 4, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, + "rest": { "index": 5, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, + "routeConfiguration": { "index": 6, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, + "routeTemplate": { "index": 7, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, + "templatedRoute": { "index": 8, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, + "route": { "index": 9, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/camel.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/camel.json index 94b3cebf509f3..1ac564378c757 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/camel.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/camel.json @@ -14,14 +14,13 @@ "properties": { "component-scan": { "index": 0, "kind": "element", "displayName": "Component-scan", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Component scanning that can auto-discover Camel route builders from the classpath." }, "bean": { "index": 1, "kind": "element", "displayName": "Bean", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "List of bean definitions to be registered in the Camel registry." }, - "semantic": { "index": 2, "kind": "element", "displayName": "Semantic", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.app.SemanticDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic question declarations." }, - "sslContextParameters": { "index": 3, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, - "dataFormat": { "index": 4, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, - "restConfiguration": { "index": 5, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, - "rest": { "index": 6, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, - "routeConfiguration": { "index": 7, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, - "routeTemplate": { "index": 8, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, - "templatedRoute": { "index": 9, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, - "route": { "index": 10, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } + "sslContextParameters": { "index": 2, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, + "dataFormat": { "index": 3, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, + "restConfiguration": { "index": 4, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, + "rest": { "index": 5, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, + "routeConfiguration": { "index": 6, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, + "routeTemplate": { "index": 7, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, + "templatedRoute": { "index": 8, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, + "route": { "index": 9, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticDefinition.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticDefinition.json deleted file mode 100644 index e66c595edd178..0000000000000 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticDefinition.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "model": { - "kind": "model", - "name": "semanticDefinition", - "title": "Semantic Definition", - "description": "Named, provider-independent semantic question declarations.", - "deprecated": false, - "label": "configuration", - "javaType": "org.apache.camel.model.app.SemanticDefinition", - "abstract": false, - "input": false, - "output": false - }, - "properties": { - "question": { "index": 0, "kind": "element", "displayName": "Question", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic questions shared by routes in this Camel context." } - } -} diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticQuestionDefinition.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticQuestionDefinition.json deleted file mode 100644 index a530faab49795..0000000000000 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models-app/semanticQuestionDefinition.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "model": { - "kind": "model", - "name": "semanticQuestionDefinition", - "title": "Semantic Question Definition", - "description": "A semantic question with the same fields and policies as a YAML declaration.", - "deprecated": false, - "label": "configuration", - "javaType": "org.apache.camel.model.app.SemanticQuestionDefinition", - "abstract": false, - "input": false, - "output": false - }, - "properties": { - "name": { "index": 0, "kind": "attribute", "displayName": "Name", "group": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The context-wide question name." }, - "type": { "index": 1, "kind": "attribute", "displayName": "Type", "group": "common", "required": true, "type": "enum", "javaType": "java.lang.String", "enum": [ "boolean", "choice", "score" ], "deprecated": false, "autowired": false, "secret": false, "description": "The question type." }, - "state": { "index": 2, "kind": "attribute", "displayName": "State", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The Simple expression selecting the message state." }, - "threshold": { "index": 3, "kind": "attribute", "displayName": "Threshold", "group": "common", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "0.5", "description": "The boolean decision threshold." }, - "uncertainty": { "index": 4, "kind": "attribute", "displayName": "Uncertainty", "group": "common", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "0", "description": "The boolean uncertainty band." }, - "uncertaintyPolicy": { "index": 5, "kind": "attribute", "displayName": "Uncertainty Policy", "group": "common", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "fail", "non-match" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "fail", "description": "The boolean uncertainty policy." }, - "instructions": { "index": 6, "kind": "element", "displayName": "Instructions", "group": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Instructions describing the judgment to make." }, - "criterion": { "index": 7, "kind": "element", "displayName": "Criterion", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Named choice criteria, or optional true\/false boolean criteria." }, - "level": { "index": 8, "kind": "element", "displayName": "Level", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Ordered descriptive levels for a score question." } - } -} diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/routes.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/routes.json index ce9cc06276050..ec9c05fa2baf7 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/routes.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/models/routes.json @@ -15,7 +15,6 @@ "id": { "index": 0, "kind": "attribute", "displayName": "Id", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The id of this node" }, "note": { "index": 1, "kind": "attribute", "displayName": "Note", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The note for this node" }, "description": { "index": 2, "kind": "attribute", "displayName": "Description", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The description for this node" }, - "routes": { "index": 3, "kind": "element", "displayName": "Routes", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "oneOf": [ "route" ], "deprecated": false, "autowired": false, "secret": false, "description": "Contains the Camel routes" }, - "semantic": { "index": 4, "kind": "element", "displayName": "Semantic", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.app.SemanticDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic question declarations." } + "routes": { "index": 3, "kind": "element", "displayName": "Routes", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "oneOf": [ "route" ], "deprecated": false, "autowired": false, "secret": false, "description": "Contains the Camel routes" } } } diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd index e30288e5c9899..ac1f0016ee7c6 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-spring.xsd @@ -12898,38 +12898,11 @@ Description of the template parameter for documentation purposes. - - - - - - - - - - - - - - - - - - - - - - - - - @@ -14961,15 +14934,6 @@ Component scanning that can auto-discover Camel route builders from the classpat - - - - - - - diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd index 63270474b9b60..c35e47ebc6bf5 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camel-xml-io.xsd @@ -12041,38 +12041,11 @@ Description of the template parameter for documentation purposes. - - - - - - - - - - - - - - - - - - - - - - - - - @@ -14104,15 +14077,6 @@ Component scanning that can auto-discover Camel route builders from the classpat - - - - - - - diff --git a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesXmlSchemaTest.java b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesXmlSchemaTest.java index 62e6f60d0b98b..106e8f40065f5 100644 --- a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesXmlSchemaTest.java +++ b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/DocExamplesXmlSchemaTest.java @@ -108,7 +108,8 @@ class DocExamplesXmlSchemaTest { * tag, a Spring bean next to route fragments, and the {@code } child of an expression, which the model * and the xml-io parser support but no generated schema can express next to the expression text (JAXB * {@code @XmlValue}), so Spring XML declares the namespaces as {@code xmlns:} attributes instead; and the endpoint - * page's unescaped {@code &}, which is there to show the error it causes. + * page's unescaped {@code &}, which is there to show the error it causes. Semantic declaration documents use a + * component-owned loader outside the core schemas and are covered by camel-semantic's loader tests. */ private static final Map EXAMPLES_SKIPPED = Map.of( "xmlsecurity-sign-component", ""); private static CamelCatalog catalog; private static Schema springSchema; diff --git a/components/camel-ai/camel-semantic/pom.xml b/components/camel-ai/camel-semantic/pom.xml index 25e764518c153..adb89ba78764c 100644 --- a/components/camel-ai/camel-semantic/pom.xml +++ b/components/camel-ai/camel-semantic/pom.xml @@ -39,22 +39,27 @@ org.apache.camel - camel-yaml-dsl-common + camel-core-model true org.apache.camel - camel-test-junit6 - test + camel-xml-io + true org.apache.camel - camel-xml-io-dsl + camel-yaml-dsl-common + true + + + org.apache.camel + camel-test-junit6 test org.apache.camel - camel-xml-jaxb-dsl + camel-xml-io-dsl test diff --git a/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/routes-loader/semantic-xml b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/routes-loader/semantic-xml new file mode 100644 index 0000000000000..dfc2945293cc8 --- /dev/null +++ b/components/camel-ai/camel-semantic/src/generated/resources/META-INF/services/org/apache/camel/routes-loader/semantic-xml @@ -0,0 +1,2 @@ +# Generated by camel build tools - do NOT edit this file! +class=org.apache.camel.semantic.SemanticXmlRoutesBuilderLoader 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 c1a09c1859366..cc9d87d92481b 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 @@ -100,19 +100,23 @@ Loading declarations does not perform inference. === Java declarations -Use `semanticQuestions().question(name)` in a `RouteBuilder` to declare questions alongside routes: +Use the fluent helper from `camel-semantic` inside an ordinary `RouteBuilder`: [source,java] ---- +import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions; + @Override public void configure() { - semanticQuestions().question("department") - .type("choice") - .state("${header.myState}") - .instructions("Which department should handle this message?") - .criterion("billing", "Invoices, payments, and refunds") - .criterion("technical", "Bugs, outages, and technical problems") - .criterion("other", "Everything else"); + semanticQuestions(this) + .question("department") + .type("choice") + .state("${header.myState}") + .instructions("Which department should handle this message?") + .criterion("billing", "Invoices, payments, and refunds") + .criterion("technical", "Bugs, outages, and technical problems") + .criterion("other", "Everything else") + .register(); from("direct:tickets") .setProperty("department").language("semantic", "ref:department") @@ -120,28 +124,34 @@ public void configure() { } ---- -Declarations are installed after `configure()` completes and before the routes are initialized. -They can appear before or after the routes that reference them within the same builder. -When adding builders to a running context, add the builder declaring shared questions before -builders whose routes reference them. Use `.type("boolean")` with -optional `.threshold(0.8)`, `.uncertainty(0.1)` and `.uncertaintyPolicy("non-match")` for boolean -questions. Use `.type("score")` and successive `.level("description")` calls for ordered score -levels. Java applications can also continue to register immutable `SemanticQuestion` definitions -using `SemanticQuestions.get(context).replace(source, questions)`. +Call `.end().question("anotherName")` to add another question to the group, then `.register()` +once to validate and install the entire group atomically. Register before creating expressions +that refer to these names. When adding builders to a running context, add the builder declaring +shared questions before builders whose routes reference them. + +Use `.type("boolean")` with optional `.threshold(0.8)`, `.uncertainty(0.1)` and +`.uncertaintyPolicy("non-match")` for boolean questions. Use `.type("score")` and successive +`.level("description")` calls for ordered score levels. Applications can also register immutable +`SemanticQuestion` definitions using `SemanticQuestions.get(context).replace(source, questions)`. -Java/XML declarations reserve source keys beginning with `model:`. For a route resource, the -key is `"model:" + resource.getLocation()`; passing this key and an empty map to `replace` -removes that resource's questions. Reloading the resource installs them again. Embedded Java -builders without a resource receive distinct generated source keys. +Each Java resource owns one group. Register all of its questions together; registering again +replaces that resource's previous group. Reloading a Java resource without the helper removes +its declarations. Embedded builders without a resource receive distinct generated source keys. +The helper reserves `"java:" + resource.getLocation()` as the resource source key; passing that +key and an empty map to `replace` removes the resource's questions. === XML declarations -With xref:others:java-xml-io-dsl.adoc[XML DSL], place a `semantic` element inside the `camel` or -`routes` document, before the routes to comply with the XML schema: +The optional XML extension belongs to `camel-semantic`. Include `camel-semantic` and +xref:others:java-xml-io-dsl.adoc[XML DSL] (`camel-xml-io-dsl`), and give declaration files the +extension `*.semantic.xml`. This selects the component's loader without changing the standard +XML loader or Camel core model. + +Both layouts are supported. To keep declarations alongside routes, use `tickets.semantic.xml`: [source,xml] ---- - + Which department should handle this message? @@ -150,51 +160,67 @@ With xref:others:java-xml-io-dsl.adoc[XML DSL], place a `semantic` element insid - + ref:department - + ---- -For boolean questions, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional question -attributes. For score questions, replace the named `criterion` elements with ordered -`level` elements, such as `RoutineUrgentCritical`. -XML declarations are loaded before route references are initialized, including those in another -resource loaded together. Reload replaces the source's questions; -removing its declarations or deleting the resource removes obsolete definitions. +To share declarations across route files, use a standalone `questions.semantic.xml`: + +[source,xml] +---- + + + Which department should handle this message? + + + + + +---- -The JAXB XML loader also supports declarations inside a `routes` document using the -`http://camel.apache.org/schema/spring` namespace. With JAXB, load a separate declaration -resource before the routes that reference it. +Load this together with ordinary `*.xml`, Java or YAML route resources that use `ref:department`. +For example, with Camel Main, set +`camel.main.routes-include-pattern=classpath:questions.semantic.xml,classpath:routes.xml`. +Declarations are registered before consuming routes are configured. Reload replaces the source's +questions; removing the `semantic` block, using an empty block, or deleting the resource removes +obsolete definitions. XML source keys are the resource locations. + +The combined format supports a `routes` root with one optional `semantic` block and ordinary +`route` elements. The standalone format uses a `semantic` root. Namespace-free documents are +supported, as are documents consistently using `http://camel.apache.org/schema/semantic`, +`http://camel.apache.org/schema/xml-io`, or `http://camel.apache.org/schema/spring`. +These extensions do not validate against Camel's standard core XSDs. Ordinary `*.xml` files +and the JAXB loader do not accept embedded semantic declarations. + +For boolean questions, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional question +attributes. For score questions, replace the named `criterion` elements with ordered `level` +elements, such as `RoutineUrgentCritical`. The numeric `threshold` and `uncertainty` options accept property placeholders in all three DSLs. For example, Java accepts `.threshold("{{semantic.threshold:0.5}}")`, and XML accepts `threshold="{{semantic.threshold:0.5}}"`. Values are resolved and validated when declarations -are registered; Java model exports preserve the placeholder text. - -Both Java and XML declarations require `camel-semantic` and use the same context-wide registry, -validation, defaults and adapters as YAML. They do not require `camel-yaml-dsl`. Their questions -can also be selected together using `refs:name1,name2`, as described below. - -=== Exporting declarations - -Direct exports of a `RoutesDefinition` containing semantic declarations preserve them. The MCP -route transformation tool also preserves declarations in XML-to-YAML, YAML-to-XML, and -Java-to-YAML/XML conversions. YAML-to-XML and Java-to-YAML/XML conversions load a temporary -Camel context and export the registered questions with their effective numeric policies, -omitting settings that match the defaults. Numeric placeholders must resolve in that context; -the tool does not load the application's external properties. Provide a placeholder default, -such as `{{semantic.threshold:0.5}}`, or a concrete value when the property is unavailable. -Unresolved required properties fail conversion. XML-to-YAML conversion preserves placeholder -text without resolving it. - -Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only -the routes and omit the question declarations. Supply the declarations separately before -reloading those dumps. +are registered. + +Java and XML declarations use the same context-wide registry, validation, defaults and adapters +as YAML. They do not require `camel-yaml-dsl`. Their questions can also be selected together +using `refs:name1,name2`, as described below. + +=== Exporting routes + +Question declarations live outside Camel's core route model. Generic model exports and runtime +route dumps such as `camel.main.dumpRoutes=yaml` contain only routes; keep declarations separately +and load them before reloading an exported route. + +The MCP route conversion tool rejects Java or YAML inputs containing semantic declarations +because a generic route export would lose those declarations. Convert the routes separately. +Extended XML documents must also be separated into declarations and ordinary routes before +using the generic converter. === Selected state diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java deleted file mode 100644 index 909454e17faf7..0000000000000 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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.semantic; - -import java.util.LinkedHashMap; -import java.util.Locale; -import java.util.Map; - -import org.apache.camel.CamelContext; -import org.apache.camel.model.PropertyDefinition; -import org.apache.camel.model.app.SemanticDefinition; -import org.apache.camel.model.app.SemanticQuestionDefinition; -import org.apache.camel.model.spi.SemanticDefinitionConfigurer; -import org.apache.camel.spi.Resource; -import org.apache.camel.util.StringHelper; - -/** Converts Java/XML declarations to the same immutable questions used by YAML and programmatic registration. */ -public class DefaultSemanticDefinitionConfigurer implements SemanticDefinitionConfigurer { - @Override - public void configure(CamelContext context, Resource resource, String source, SemanticDefinition definition) { - Map questions = new LinkedHashMap<>(); - if (definition != null) { - for (SemanticQuestionDefinition question : definition.getQuestions()) { - String name = question.getName(); - if (name == null || name.isBlank()) { - throw new IllegalArgumentException("Semantic question requires a nonblank name"); - } - if (questions.containsKey(name)) { - throw new IllegalArgumentException("Duplicate semantic question: " + name); - } - try { - questions.put(name, question(context, question)); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid semantic question '" + name + "': " + e.getMessage(), e); - } - } - } - // A YAML loader can register its own declarations from the same resource before its RouteBuilder runs. - SemanticQuestions.get(context).replace("model:" + source, resource, questions); - } - - @Override - public SemanticDefinition getDefinition(CamelContext context) { - SemanticQuestions questions = context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class); - if (questions == null) { - return null; - } - SemanticDefinition definition = new SemanticDefinition(); - questions.snapshot().entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> { - SemanticQuestion question = entry.getValue(); - SemanticQuestionDefinition target = definition.question(entry.getKey()) - .type(question.getType().name().toLowerCase(Locale.ROOT)) - .instructions(question.getInstructions()).state(question.getState()); - question.getCriteria().forEach(target::criterion); - question.getLevels().forEach(target::level); - if (question.getType() == SemanticQuestion.Type.BOOLEAN) { - if (question.getThreshold() != 0.5) { - target.threshold(question.getThreshold()); - } - if (question.getUncertainty() != 0) { - target.uncertainty(question.getUncertainty()); - } - if (question.getUncertaintyPolicy() != SemanticQuestion.UncertaintyPolicy.FAIL) { - target.uncertaintyPolicy(question.getUncertaintyPolicy().name().toLowerCase(Locale.ROOT).replace('_', '-')); - } - } - }); - return definition; - } - - private static SemanticQuestion question(CamelContext context, SemanticQuestionDefinition definition) { - if (definition.getType() == null) { - throw new IllegalArgumentException("Question type is required"); - } - SemanticQuestion.Type type = enumeration(definition.getType(), SemanticQuestion.Type.class); - if (type != SemanticQuestion.Type.BOOLEAN - && (definition.getThreshold() != null || definition.getUncertainty() != null - || definition.getUncertaintyPolicy() != null)) { - throw new IllegalArgumentException("Threshold and uncertainty policy require a boolean question"); - } - Map criteria = new LinkedHashMap<>(); - for (PropertyDefinition criterion : definition.getCriteria()) { - if (criteria.containsKey(criterion.getKey())) { - throw new IllegalArgumentException("Duplicate semantic criterion: " + criterion.getKey()); - } - criteria.put(criterion.getKey(), criterion.getValue()); - } - return new SemanticQuestion( - type, definition.getInstructions(), definition.getState(), criteria, - definition.getLevels(), - definition.getThreshold() == null ? 0.5 : parseDouble(context, definition.getThreshold(), "threshold"), - definition.getUncertainty() == null ? 0 : parseDouble(context, definition.getUncertainty(), "uncertainty"), - definition.getUncertaintyPolicy() == null - ? SemanticQuestion.UncertaintyPolicy.FAIL - : enumeration(definition.getUncertaintyPolicy(), SemanticQuestion.UncertaintyPolicy.class)); - } - - private static double parseDouble(CamelContext context, String value, String field) { - try { - return Double.parseDouble(context.resolvePropertyPlaceholders(value)); - } catch (NumberFormatException e) { - throw new IllegalArgumentException(field + " must be a valid number: " + value, e); - } - } - - private static > T enumeration(String value, Class type) { - String normalized = StringHelper.asEnumConstantValue(value); - for (T constant : type.getEnumConstants()) { - if (constant.name().equalsIgnoreCase(value) || constant.name().equalsIgnoreCase(normalized)) { - return constant; - } - } - throw new IllegalArgumentException("Invalid " + type.getSimpleName() + ": " + value); - } -} diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java new file mode 100644 index 0000000000000..584837ba56979 --- /dev/null +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionBuilder.java @@ -0,0 +1,149 @@ +/* + * 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.semantic; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.util.StringHelper; + +/** Fluent definition of a named semantic question. */ +public final class SemanticQuestionBuilder { + private final SemanticQuestionsBuilder parent; + private final Map criteria = new LinkedHashMap<>(); + private final List levels = new ArrayList<>(); + private String type; + private String instructions; + private String state; + private String threshold; + private String uncertainty; + private String uncertaintyPolicy; + + SemanticQuestionBuilder(SemanticQuestionsBuilder parent) { + this.parent = parent; + } + + /** The question type: boolean, choice or score. */ + public SemanticQuestionBuilder type(String type) { + this.type = type; + return this; + } + + /** The instructions sent to the provider. */ + public SemanticQuestionBuilder instructions(String instructions) { + this.instructions = instructions; + return this; + } + + /** A Simple expression selecting the state to evaluate; defaults to the message body. */ + public SemanticQuestionBuilder state(String state) { + this.state = state; + return this; + } + + /** Add a named choice, or a true/false criterion for a boolean question. */ + public SemanticQuestionBuilder criterion(String name, String description) { + if (criteria.containsKey(name)) { + throw new IllegalArgumentException("Duplicate semantic criterion: " + name); + } + criteria.put(name, description); + return this; + } + + /** Add an ordered score level. */ + public SemanticQuestionBuilder level(String level) { + levels.add(level); + return this; + } + + /** Boolean decision threshold; defaults to 0.5. */ + public SemanticQuestionBuilder threshold(double threshold) { + return threshold(Double.toString(threshold)); + } + + /** Boolean decision threshold, optionally using property placeholders. */ + public SemanticQuestionBuilder threshold(String threshold) { + this.threshold = threshold; + return this; + } + + /** Boolean uncertainty band; defaults to zero. */ + public SemanticQuestionBuilder uncertainty(double uncertainty) { + return uncertainty(Double.toString(uncertainty)); + } + + /** Boolean uncertainty band, optionally using property placeholders. */ + public SemanticQuestionBuilder uncertainty(String uncertainty) { + this.uncertainty = uncertainty; + return this; + } + + /** Boolean uncertainty policy: fail (default) or non-match. */ + public SemanticQuestionBuilder uncertaintyPolicy(String uncertaintyPolicy) { + this.uncertaintyPolicy = uncertaintyPolicy; + return this; + } + + /** Return to the group to add another question. */ + public SemanticQuestionsBuilder end() { + return parent; + } + + /** Validate and register the entire group. */ + public void register() { + parent.register(); + } + + SemanticQuestion build(CamelContext context) { + if (type == null) { + throw new IllegalArgumentException("Question type is required"); + } + SemanticQuestion.Type questionType = enumeration(type, SemanticQuestion.Type.class); + if (questionType != SemanticQuestion.Type.BOOLEAN + && (threshold != null || uncertainty != null || uncertaintyPolicy != null)) { + throw new IllegalArgumentException("Threshold and uncertainty policy require a boolean question"); + } + return new SemanticQuestion( + questionType, instructions, state, criteria, levels, + threshold == null ? 0.5 : parseDouble(context, threshold, "threshold"), + uncertainty == null ? 0 : parseDouble(context, uncertainty, "uncertainty"), + uncertaintyPolicy == null + ? SemanticQuestion.UncertaintyPolicy.FAIL + : enumeration(uncertaintyPolicy, SemanticQuestion.UncertaintyPolicy.class)); + } + + private static double parseDouble(CamelContext context, String value, String field) { + try { + return Double.parseDouble(context.resolvePropertyPlaceholders(value)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(field + " must be a valid number: " + value, e); + } + } + + private static > T enumeration(String value, Class type) { + String normalized = StringHelper.asEnumConstantValue(value); + for (T constant : type.getEnumConstants()) { + if (constant.name().equalsIgnoreCase(value) || constant.name().equalsIgnoreCase(normalized)) { + return constant; + } + } + throw new IllegalArgumentException("Invalid " + type.getSimpleName() + ": " + value); + } +} diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java index 5114f0107972e..037e9b67df7bd 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java @@ -44,9 +44,9 @@ public static SemanticQuestions get(CamelContext context) { } /** - * Replace all definitions from one source; an empty map removes obsolete declarations. Java/XML declarations - * reserve the {@code model:} prefix. Resource declarations use {@code model:} followed by - * {@link Resource#getLocation()}, while embedded Java builders use unique generated source keys. + * Replace all definitions from one source; an empty map removes obsolete declarations. The Java fluent helper + * reserves {@code java:} followed by the resource location (or a generated key for embedded builders). XML and YAML + * declarations use the resource location as their source key. */ public synchronized void replace(String source, Map definitions) { Map replacement = new HashMap<>(); @@ -90,8 +90,15 @@ synchronized void removeDeletedResources() { .forEach(location -> replace(location, Map.of())); } - Map snapshot() { - return questions; + synchronized void remove(String source) { + if (sources.containsKey(source)) { + replace(source, Map.of()); + } + } + + /** Whether any named questions have been registered. */ + public boolean isEmpty() { + return questions.isEmpty(); } public SemanticQuestion get(String name) { diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java new file mode 100644 index 0000000000000..b2b122bd7bc0d --- /dev/null +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestionsBuilder.java @@ -0,0 +1,113 @@ +/* + * 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.semantic; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import org.apache.camel.CamelContext; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.builder.RouteBuilderLifecycleStrategy; +import org.apache.camel.spi.Resource; + +/** Fluent declarations for use inside an ordinary {@link RouteBuilder#configure()}. */ +public final class SemanticQuestionsBuilder { + private static final String LIFECYCLE = SemanticQuestionsBuilder.class.getName(); + private final CamelContext context; + private final RouteBuilder builder; + private final Resource resource; + private final String source; + private final Map questions = new LinkedHashMap<>(); + + private SemanticQuestionsBuilder(RouteBuilder builder) { + this(builder.getContext(), builder.getResource(), builder.getResource() == null + ? "java:" + builder.getContext().getUuidGenerator().generateUuid() + : "java:" + builder.getResource().getLocation(), + builder); + } + + SemanticQuestionsBuilder(CamelContext context, Resource resource, String source, RouteBuilder builder) { + this.context = context; + this.resource = resource; + this.source = source; + this.builder = builder; + } + + /** Start one group of declarations, then call {@link #register()} before using its references. */ + public static SemanticQuestionsBuilder semanticQuestions(RouteBuilder builder) { + return new SemanticQuestionsBuilder(builder); + } + + /** Add a named question. Names must be unique across the context. */ + public SemanticQuestionBuilder question(String name) { + if (name == null || name.isBlank()) { + throw new IllegalArgumentException("Semantic question requires a nonblank name"); + } + SemanticQuestionBuilder question = new SemanticQuestionBuilder(this); + if (questions.putIfAbsent(name, question) != null) { + throw new IllegalArgumentException("Duplicate semantic question: " + name); + } + return question; + } + + /** Validate all declarations and atomically replace this source's questions. An empty group removes them. */ + public void register() { + Map definitions = new LinkedHashMap<>(); + questions.forEach((name, question) -> { + try { + definitions.put(name, question.build(context)); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid semantic question '" + name + "': " + e.getMessage(), e); + } + }); + SemanticQuestions registry = SemanticQuestions.get(context); + synchronized (registry) { + registry.replace(source, resource, definitions); + DeclarationsLifecycle lifecycle = context.getRegistry().lookupByNameAndType(LIFECYCLE, DeclarationsLifecycle.class); + if (lifecycle == null) { + lifecycle = new DeclarationsLifecycle(registry); + context.getRegistry().bind(LIFECYCLE, lifecycle); + } + if (builder != null) { + lifecycle.registered.add(builder); + } + } + } + + // A resource may remove the helper entirely on reload. The existing builder lifecycle detects that case. + private static final class DeclarationsLifecycle implements RouteBuilderLifecycleStrategy { + private final SemanticQuestions questions; + private final Set registered = Collections.newSetFromMap(new WeakHashMap<>()); + + private DeclarationsLifecycle(SemanticQuestions questions) { + this.questions = questions; + } + + @Override + public void afterConfigure(RouteBuilder builder) { + synchronized (questions) { + questions.removeDeletedResources(); + if (!registered.remove(builder) && builder.getResource() != null) { + questions.remove("java:" + builder.getResource().getLocation()); + } + } + } + } +} diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java new file mode 100644 index 0000000000000..a4c6b0774d508 --- /dev/null +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java @@ -0,0 +1,216 @@ +/* + * 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.semantic; + +import java.io.InputStream; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import org.apache.camel.CamelContextAware; +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.RoutesDefinition; +import org.apache.camel.spi.Resource; +import org.apache.camel.spi.annotations.RoutesLoader; +import org.apache.camel.support.RoutesBuilderLoaderSupport; +import org.apache.camel.xml.in.ModelParser; + +/** Loads standalone declarations or declarations alongside routes from {@code *.semantic.xml} resources. */ +@RoutesLoader("semantic.xml") +public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport { + private static final Set NAMESPACES = Set.of("", "http://camel.apache.org/schema/semantic", + "http://camel.apache.org/schema/xml-io", "http://camel.apache.org/schema/spring"); + + @Override + public String getSupportedExtension() { + return "semantic.xml"; + } + + @Override + public void preParseRoute(Resource resource) throws Exception { + // Register before any consuming route (including another DSL/resource) is configured. + parse(resource); + } + + @Override + public RoutesBuilder loadRoutesBuilder(Resource resource) throws Exception { + RoutesDefinition routes = parse(resource); + RouteBuilder builder = new RouteBuilder(getCamelContext()) { + @Override + public void configure() { + routes.getRoutes().forEach(route -> { + CamelContextAware.trySetCamelContext(route, getContext()); + getRouteCollection().route(route); + }); + } + }; + builder.setResource(resource); + return builder; + } + + private RoutesDefinition parse(Resource resource) throws Exception { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + Element root; + try (InputStream stream = resource.getInputStream()) { + root = factory.newDocumentBuilder().parse(stream).getDocumentElement(); + } + String namespace = namespace(root); + if (!NAMESPACES.contains(namespace)) { + throw new IllegalArgumentException("Unsupported semantic XML namespace: " + namespace); + } + SemanticQuestionsBuilder questions + = new SemanticQuestionsBuilder(getCamelContext(), resource, resource.getLocation(), null); + RoutesDefinition routes = new RoutesDefinition(); + if ("semantic".equals(root.getLocalName())) { + declarations(root, questions); + } else if ("routes".equals(root.getLocalName())) { + boolean found = false; + for (Element child : children(root)) { + if ("semantic".equals(child.getLocalName())) { + if (found) { + throw new IllegalArgumentException("Only one semantic declaration block is allowed"); + } + found = true; + declarations(child, questions); + root.removeChild(child); + } else if (!"route".equals(child.getLocalName())) { + throw new IllegalArgumentException("Unexpected element in routes: " + child.getTagName()); + } + } + TransformerFactory transformer = TransformerFactory.newInstance(); + transformer.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + transformer.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + transformer.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + StringWriter xml = new StringWriter(); + transformer.newTransformer().transform(new DOMSource(root), new StreamResult(xml)); + routes = new ModelParser(new StringReader(xml.toString()), namespace).parseRoutesDefinition() + .orElseThrow(() -> new IllegalArgumentException("Expected XML routes")); + } else { + throw new IllegalArgumentException("Expected semantic or routes root element"); + } + // Parse the complete document and validate every question before publishing any definitions. + questions.register(); + return routes; + } + + private static void declarations(Element semantic, SemanticQuestionsBuilder questions) { + attributes(semantic, Set.of()); + for (Element element : children(semantic)) { + if (!"question".equals(element.getLocalName())) { + throw new IllegalArgumentException("Unexpected semantic element: " + element.getTagName()); + } + attributes(element, Set.of("name", "type", "state", "threshold", "uncertainty", "uncertaintyPolicy")); + SemanticQuestionBuilder question = questions.question(element.getAttribute("name")); + if (element.hasAttribute("type")) { + question.type(element.getAttribute("type")); + } + if (element.hasAttribute("state")) { + question.state(element.getAttribute("state")); + } + if (element.hasAttribute("threshold")) { + question.threshold(element.getAttribute("threshold")); + } + if (element.hasAttribute("uncertainty")) { + question.uncertainty(element.getAttribute("uncertainty")); + } + if (element.hasAttribute("uncertaintyPolicy")) { + question.uncertaintyPolicy(element.getAttribute("uncertaintyPolicy")); + } + boolean instructions = false; + for (Element child : children(element)) { + switch (child.getLocalName()) { + case "instructions" -> { + if (instructions) { + throw new IllegalArgumentException("Duplicate instructions for semantic question"); + } + instructions = true; + attributes(child, Set.of()); + question.instructions(text(child)); + } + case "criterion" -> { + attributes(child, Set.of("key", "value")); + if (!children(child).isEmpty()) { + throw new IllegalArgumentException("Semantic criterion must not contain elements"); + } + question.criterion(child.getAttribute("key"), child.getAttribute("value")); + } + case "level" -> { + attributes(child, Set.of()); + question.level(text(child)); + } + default -> throw new IllegalArgumentException("Unexpected question element: " + child.getTagName()); + } + } + } + } + + private static String text(Element element) { + for (Node child = element.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child instanceof Element) { + throw new IllegalArgumentException("Unexpected element in " + element.getTagName()); + } + } + return element.getTextContent(); + } + + private static List children(Element parent) { + List answer = new ArrayList<>(); + for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { + if (child instanceof Element element) { + if (!namespace(parent).equals(namespace(element))) { + throw new IllegalArgumentException("Unexpected namespace on " + element.getTagName()); + } + answer.add(element); + } else if ((child.getNodeType() == Node.TEXT_NODE || child.getNodeType() == Node.CDATA_SECTION_NODE) + && !child.getTextContent().isBlank()) { + throw new IllegalArgumentException("Unexpected text in " + parent.getTagName()); + } + } + return answer; + } + + private static void attributes(Element element, Set allowed) { + for (int i = 0; i < element.getAttributes().getLength(); i++) { + Node attribute = element.getAttributes().item(i); + if (!XMLConstants.XMLNS_ATTRIBUTE_NS_URI.equals(attribute.getNamespaceURI()) + && (attribute.getNamespaceURI() != null || !allowed.contains(attribute.getNodeName()))) { + throw new IllegalArgumentException("Unexpected attribute: " + attribute.getNodeName()); + } + } + } + + private static String namespace(Element element) { + return element.getNamespaceURI() == null ? "" : element.getNamespaceURI(); + } +} diff --git a/components/camel-ai/camel-semantic/src/main/resources/META-INF/services/org/apache/camel/semantic-configurer b/components/camel-ai/camel-semantic/src/main/resources/META-INF/services/org/apache/camel/semantic-configurer deleted file mode 100644 index 9d239b7c3eee0..0000000000000 --- a/components/camel-ai/camel-semantic/src/main/resources/META-INF/services/org/apache/camel/semantic-configurer +++ /dev/null @@ -1 +0,0 @@ -class=org.apache.camel.semantic.DefaultSemanticDefinitionConfigurer diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java index 6115b5a8c4428..8a396b98c76fe 100644 --- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java +++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java @@ -25,12 +25,8 @@ import org.apache.camel.Exchange; import org.apache.camel.builder.RouteBuilder; -import org.apache.camel.dsl.support.RouteBuilderLoaderSupport; -import org.apache.camel.dsl.xml.jaxb.JaxbXmlRoutesBuilderLoader; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.language.semantic.SemanticLanguage; -import org.apache.camel.model.SemanticDefinitionHelper; -import org.apache.camel.model.app.SemanticDefinition; import org.apache.camel.spi.Resource; import org.apache.camel.support.DefaultExchange; import org.apache.camel.support.PluginHelper; @@ -45,6 +41,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -81,7 +78,7 @@ void close() throws Exception { context.close(); } - private static void questions(SemanticDefinition semantic) { + private static void questions(SemanticQuestionsBuilder semantic) { semantic.question("urgent").type("boolean").instructions("Is this urgent?") .state("${header.myState}").threshold(0.8).uncertainty(0.1).uncertaintyPolicy("non-match"); semantic.question("department").type("choice").instructions("Which department?") @@ -89,6 +86,7 @@ private static void questions(SemanticDefinition semantic) { .criterion("technical", "Bugs and outages"); semantic.question("priority").type("score").instructions("How urgent?") .state("${header.myState}").level("Routine").level("Urgent").level("Critical"); + semantic.register(); } private static String xmlQuestions() { @@ -116,8 +114,8 @@ private static String document(String root, String contents) { } @ParameterizedTest - @ValueSource(strings = { "java", "camel", "routes", "jaxb" }) - void nativeDeclarationsEvaluateSingleQuestionsAndMixedBatches(String dsl) throws Exception { + @ValueSource(strings = { "java", "routes", "standalone", "no-namespace", "component-namespace", "xml-io-namespace" }) + void componentDeclarationsEvaluateSingleQuestionsAndMixedBatches(String dsl) throws Exception { if (dsl.equals("java")) { context.addRoutes(new RouteBuilder() { @Override @@ -125,7 +123,7 @@ public void configure() { from("direct:batch").setProperty("decision").language("semantic", "refs:urgent,department,priority") .setHeader("department", simple("${exchangeProperty.decision[department]}")); // Declarations are collected before route initialization, including those after the route. - questions(semanticQuestions()); + questions(semanticQuestions(this)); } }); } else { @@ -138,15 +136,20 @@ public void configure() { ${exchangeProperty.decision[department]} """; - if (dsl.equals("jaxb")) { - try (var loader = new JaxbXmlRoutesBuilderLoader()) { - loader.setCamelContext(context); - loader.start(); - context.addRoutes(loader.loadRoutesBuilder( - ResourceHelper.fromString("questions.xml", document("routes", route + xmlQuestions())))); - } + if (dsl.equals("standalone")) { + PluginHelper.getRoutesLoader(context).loadRoutes(List.of( + ResourceHelper.fromString("use.xml", document("routes", route)), + ResourceHelper.fromString("questions.semantic.xml", xmlQuestions()))); } else { - load("questions.xml", document(dsl, route + xmlQuestions())); + String xml = document("routes", route + xmlQuestions()); + if (dsl.equals("no-namespace")) { + xml = xml.replace(" xmlns=\"http://camel.apache.org/schema/spring\"", ""); + } else if (dsl.equals("component-namespace")) { + xml = xml.replace("schema/spring", "schema/semantic"); + } else if (dsl.equals("xml-io-namespace")) { + xml = xml.replace("schema/spring", "schema/xml-io"); + } + load("questions.semantic.xml", xml); } } assertThat(states).isEmpty(); @@ -177,14 +180,15 @@ public void configure() { @ParameterizedTest @MethodSource("invalidXml") void invalidDeclarationsLeaveThePreviousDefinitionsIntact(String declaration, String message) throws Exception { - load("questions.xml", document("routes", xmlQuestions())); + load("questions.semantic.xml", document("routes", xmlQuestions())); SemanticQuestion previous = SemanticQuestions.get(context).get("department"); assertThatThrownBy(() -> PluginHelper.getRoutesLoader(context).updateRoutes( - ResourceHelper.fromString("questions.xml", document("routes", "" + declaration + "")))) + ResourceHelper.fromString("questions.semantic.xml", + document("routes", "" + declaration + "")))) .hasMessageContaining(message); assertThat(SemanticQuestions.get(context).get("department")).isSameAs(previous); assertThat(states).isEmpty(); - PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.xml", + PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.semantic.xml", document("routes", xmlQuestions().replace("header.myState", "header.corrected")))); assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${header.corrected}"); } @@ -226,7 +230,7 @@ class Questions extends RouteBuilder { @Override public void configure() { if (name != null) { - semanticQuestions().question(name).type("boolean").instructions("Valid?"); + semanticQuestions(this).question(name).type("boolean").instructions("Valid?").register(); } } } @@ -239,24 +243,6 @@ public void configure() { assertThat(states).isEmpty(); } - @ParameterizedTest - @ValueSource(booleans = { false, true }) - void declarationSourceCanBeReplacedAndRemovedIndependentlyOfItsResource(boolean trackResource) { - Resource resource = trackResource ? ResourceHelper.fromString("questions.xml", "") : null; - SemanticDefinition definition = new SemanticDefinition(); - definition.question("first").type("boolean").instructions("Valid?"); - SemanticDefinitionHelper.configure(context, resource, "declared-source", definition); - SemanticDefinition replacement = new SemanticDefinition(); - replacement.question("second").type("boolean").instructions("Updated?"); - SemanticDefinitionHelper.configure(context, resource, "declared-source", replacement); - SemanticQuestions questions = SemanticQuestions.get(context); - assertThatThrownBy(() -> questions.get("first")).hasMessageContaining("Unknown"); - assertThat(questions.get("second")).isNotNull(); - questions.replace("model:declared-source", Map.of()); - assertThatThrownBy(() -> questions.get("second")).hasMessageContaining("Unknown"); - assertThat(states).isEmpty(); - } - @Test void embeddedBuilderInstancesOwnSeparateDeclarations() throws Exception { class Questions extends RouteBuilder { @@ -269,7 +255,7 @@ class Questions extends RouteBuilder { @Override public void configure() { if (name != null) { - semanticQuestions().question(name).type("boolean").instructions("Valid?"); + semanticQuestions(this).question(name).type("boolean").instructions("Valid?").register(); } } } @@ -287,7 +273,8 @@ void policySpellingsMatchYaml(String policy) throws Exception { context.addRoutes(new RouteBuilder() { @Override public void configure() { - semanticQuestions().question("urgent").type("BOOLEAN").instructions("Urgent?").uncertaintyPolicy(policy); + semanticQuestions(this).question("urgent").type("BOOLEAN").instructions("Urgent?").uncertaintyPolicy(policy) + .register(); } }); assertThat(SemanticQuestions.get(context).get("urgent").getUncertaintyPolicy()) @@ -299,10 +286,10 @@ void duplicateNamesAcrossJavaAndXmlAreRejected() throws Exception { context.addRoutes(new RouteBuilder() { @Override public void configure() { - questions(semanticQuestions()); + questions(semanticQuestions(this)); } }); - assertThatThrownBy(() -> load("duplicate.xml", document("routes", xmlQuestions()))) + assertThatThrownBy(() -> load("duplicate.semantic.xml", document("routes", xmlQuestions()))) .hasMessageContaining("Duplicate semantic question"); assertThat(states).isEmpty(); } @@ -314,18 +301,18 @@ void numericPlaceholdersResolveBeforeValidation(String dsl) throws Exception { context.addRoutes(new RouteBuilder() { @Override public void configure() { - semanticQuestions().question("urgent").type("boolean").instructions("Urgent?") - .threshold("{{threshold:0.8}}").uncertainty("{{uncertainty:0.1}}"); + semanticQuestions(this).question("urgent").type("boolean").instructions("Urgent?") + .threshold("{{threshold:0.8}}").uncertainty("{{uncertainty:0.1}}").register(); } }); } else { - load("questions.xml", document("routes", xmlQuestions() + load("questions.semantic.xml", document("routes", xmlQuestions() .replace("threshold=\"0.8\"", "threshold=\"{{threshold:0.8}}\"") .replace("uncertainty=\"0.1\"", "uncertainty=\"{{uncertainty:0.1}}\""))); } assertThat(SemanticQuestions.get(context).get("urgent").getThreshold()).isEqualTo(0.8); assertThat(SemanticQuestions.get(context).get("urgent").getUncertainty()).isEqualTo(0.1); - assertThatThrownBy(() -> load("invalid.xml", document("routes", """ + assertThatThrownBy(() -> load("invalid.semantic.xml", document("routes", """ Urgent? @@ -334,106 +321,19 @@ public void configure() { .hasRootCauseInstanceOf(NumberFormatException.class); } - @ParameterizedTest - @ValueSource(strings = { "routes", "camel" }) - void repeatedUnchangedPreparsePreservesDeclarationsAndBeans(String root) throws Exception { - var loader = PluginHelper.getRoutesLoader(context).getRoutesLoader("xml"); - Resource resource = ResourceHelper.fromString("questions.xml", document(root, xmlQuestions() - + (root.equals("camel") - ? "" - : ""))); - loader.preParseRoute(resource); - SemanticQuestion question = SemanticQuestions.get(context).get("urgent"); - Object bean = context.getRegistry().lookupByName("counter"); - loader.preParseRoute(resource); - assertThat(SemanticQuestions.get(context).get("urgent")).isSameAs(question); - if (root.equals("camel")) { - assertThat(bean).isNotNull(); - assertThat(context.getRegistry().lookupByName("counter")).isSameAs(bean); - } - } - - @ParameterizedTest - @MethodSource("externalFailures") - void retryAfterExternalFailureReadsChangedXml(String root, boolean failPreparse) throws Exception { - var failingLoader = new RouteBuilderLoaderSupport("fail") { - @Override - public void preParseRoute(Resource resource) { - if (failPreparse) { - throw new IllegalStateException("foreign preparse failure"); - } - } - - @Override - public RouteBuilder doLoadRouteBuilder(Resource resource) { - return new RouteBuilder() { - @Override - public void configure() { - throw new IllegalStateException("earlier builder failure"); - } - }; - } - }; - context.getRegistry().bind("routes-builder-loader-fail", failingLoader); - Path file = directory.resolve("questions.xml"); - Files.writeString(file, document(root, xmlQuestions())); - Resource resource = ResourceHelper.resolveResource(context, file.toUri().toString()); - Resource broken = ResourceHelper.fromString("broken.fail", ""); - var loader = PluginHelper.getRoutesLoader(context); - assertThatThrownBy(() -> loader.loadRoutes(failPreparse - ? List.of(resource, broken) : List.of(broken, resource))) - .hasMessageContaining(failPreparse ? "foreign preparse failure" : "earlier builder failure"); - assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${header.myState}"); - Files.writeString(file, document(root, xmlQuestions().replace("header.myState", "header.updated") + """ - - ref:department - - """)); - loader.loadRoutes(resource); - assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${header.updated}"); - try (var template = context.createProducerTemplate()) { - assertThat(template.requestBodyAndHeader("direct:updated", "old", "updated", "new")).isEqualTo("billing"); - } - assertThat(states).containsExactly("new"); - } - - @ParameterizedTest - @ValueSource(booleans = { false, true }) - void deferredBeansKeepTheirOwningResourceWhenAnotherBuilderFails(boolean removeBean) throws Exception { - var loader = PluginHelper.getRoutesLoader(context); - Resource first = ResourceHelper.fromString("first.xml", document("camel", xmlQuestions())); - Resource second = ResourceHelper.fromString("second.xml", document("camel", """ - - """)); - assertThatThrownBy(() -> loader.loadRoutes(List.of(first, second))).hasMessageContaining("Error creating bean"); - if (removeBean) { - loader.loadRoutes(ResourceHelper.fromString("second.xml", document("camel", ""))); - assertThat(context.getRegistry().lookupByName("deferred")).isNull(); - } else { - context.getRegistry().bind("factory", new DeferredBeanFactory()); - loader.loadRoutes(second); - assertThat(context.getRegistry().lookupByName("deferred")).isEqualTo("created"); - } - } - - static Stream externalFailures() { - return Stream.of(Arguments.of("routes", true), Arguments.of("routes", false), - Arguments.of("camel", true), Arguments.of("camel", false)); - } - @Test void failedResourceBatchDoesNotReuseEarlierCachedDeclarationsOnRetry() throws Exception { String second = "" + "Valid?"; var loader = PluginHelper.getRoutesLoader(context); assertThatThrownBy(() -> loader.loadRoutes(List.of( - ResourceHelper.fromString("first.xml", document("routes", xmlQuestions())), - ResourceHelper.fromString("second.xml", document("routes", second.replace("boolean", "unknown")))))) + ResourceHelper.fromString("first.semantic.xml", document("routes", xmlQuestions())), + ResourceHelper.fromString("second.semantic.xml", document("routes", second.replace("boolean", "unknown")))))) .hasMessageContaining("Invalid semantic question"); loader.loadRoutes(List.of( - ResourceHelper.fromString("first.xml", + ResourceHelper.fromString("first.semantic.xml", document("routes", xmlQuestions().replace("header.myState", "header.new"))), - ResourceHelper.fromString("second.xml", document("routes", second)))); + ResourceHelper.fromString("second.semantic.xml", document("routes", second)))); assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${header.new}"); assertThat(SemanticQuestions.get(context).get("second")).isNotNull(); assertThat(states).isEmpty(); @@ -447,7 +347,7 @@ void declarationsFromAnotherResourceAreInstalledBeforeReferences() throws Except ref:department """)), - ResourceHelper.fromString("definitions.xml", document("camel", xmlQuestions())))); + ResourceHelper.fromString("definitions.semantic.xml", document("routes", xmlQuestions())))); assertThat(states).isEmpty(); try (var template = context.createProducerTemplate()) { assertThat(template.requestBodyAndHeader("direct:use", "original", "myState", "invoice")).isEqualTo("billing"); @@ -466,29 +366,30 @@ void unknownReferenceFailsAtRouteInitialization() { @Test void reloadingReplacesAndRemovesDeclarationsUsedByExistingExpressions() throws Exception { - load("questions.xml", document("camel", xmlQuestions())); + load("questions.semantic.xml", document("routes", xmlQuestions())); var expression = context.resolveLanguage("semantic").createExpression("ref:department"); var exchange = new DefaultExchange(context); exchange.getMessage().setHeader("myState", "old"); assertThat(expression.evaluate(exchange, String.class)).isEqualTo("billing"); - PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.xml", - document("camel", xmlQuestions().replace("header.myState", "header.updated")))); + PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.semantic.xml", + document("routes", xmlQuestions().replace("header.myState", "header.updated")))); exchange.getMessage().setHeader("updated", "new"); assertThat(expression.evaluate(exchange, String.class)).isEqualTo("billing"); assertThat(states).containsExactly("old", "new"); - PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.xml", document("camel", ""))); + PluginHelper.getRoutesLoader(context) + .updateRoutes(ResourceHelper.fromString("questions.semantic.xml", document("routes", ""))); assertThatThrownBy(() -> expression.evaluate(exchange, Object.class)).hasMessageContaining("Unknown semantic question"); } @Test void watcherRemovesDeletedAndRenamedDeclarationResources() throws Exception { - Path original = directory.resolve("questions.xml"); - Files.writeString(original, document("camel", xmlQuestions())); + Path original = directory.resolve("questions.semantic.xml"); + Files.writeString(original, document("routes", xmlQuestions())); Resource source = ResourceHelper.resolveResource(context, original.toUri().toString()); PluginHelper.getRoutesLoader(context).loadRoutes(source); TestWatcher watcher = new TestWatcher(); watcher.setCamelContext(context); - Path renamed = Files.move(original, directory.resolve("renamed.xml")); + Path renamed = Files.move(original, directory.resolve("renamed.semantic.xml")); watcher.reload(source); assertThatThrownBy(() -> SemanticQuestions.get(context).get("department")).hasMessageContaining("Unknown"); Resource replacement = ResourceHelper.resolveResource(context, renamed.toUri().toString()); @@ -500,14 +401,83 @@ void watcherRemovesDeletedAndRenamedDeclarationResources() throws Exception { assertThatThrownBy(() -> SemanticQuestions.get(context).get("department")).hasMessageContaining("Unknown"); } - private void load(String location, String xml) throws Exception { - PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString(location, xml)); + @ParameterizedTest + @ValueSource(strings = { "", "http://camel.apache.org/schema/semantic", "http://camel.apache.org/schema/xml-io" }) + void standaloneXmlSupportsOptionalNamespace(String namespace) throws Exception { + load("questions.semantic.xml", xmlQuestions().replace("", "")); + assertThat(SemanticQuestions.get(context).get("department").getCriteria()).containsKey("billing"); + load("questions.semantic.xml", ""); + assertThat(SemanticQuestions.get(context).isEmpty()).isTrue(); + } + + @Test + void failedJavaReplacementKeepsPreviousDefinitions() throws Exception { + class Questions extends RouteBuilder { + private final String instructions; + + Questions(String instructions) { + this.instructions = instructions; + setResource(ResourceHelper.fromString("Questions.java", "")); + } + + @Override + public void configure() { + semanticQuestions(this).question("urgent").type("boolean").instructions(instructions).end() + .question("other").type("boolean").instructions("Other?").register(); + } + } + context.addRoutes(new Questions("Urgent?")); + var previous = SemanticQuestions.get(context).get("urgent"); + assertThatThrownBy(() -> context.addRoutes(new Questions(" "))).hasMessageContaining("instructions must not be blank"); + assertThat(SemanticQuestions.get(context).get("urgent")).isSameAs(previous); + assertThat(SemanticQuestions.get(context).get("other")).isNotNull(); + } + + @ParameterizedTest + @ValueSource(strings = { + "", + "", + "Hi", + "]>&external;", + "Hi" + }) + void malformedXmlDoesNotReplacePreviousDefinitions(String xml) throws Exception { + load("questions.semantic.xml", xmlQuestions()); + var previous = SemanticQuestions.get(context).get("department"); + assertThatThrownBy(() -> load("questions.semantic.xml", xml)).isInstanceOf(Exception.class); + assertThat(SemanticQuestions.get(context).get("department")).isSameAs(previous); } - public static class DeferredBeanFactory { - public static String create() { - return "created"; + @ParameterizedTest + @ValueSource(strings = { "java", "xml" }) + void loadingPlainBuilderDiscardsDeletedDeclarationResources(String dsl) throws Exception { + Path file = directory.resolve(dsl.equals("java") ? "Questions.java" : "questions.semantic.xml"); + Files.writeString(file, xmlQuestions()); + Resource resource = ResourceHelper.resolveResource(context, file.toUri().toString()); + if (dsl.equals("java")) { + RouteBuilder builder = new RouteBuilder() { + @Override + public void configure() { + semanticQuestions(this).question("urgent").type("boolean").instructions("Urgent?").register(); + } + }; + builder.setResource(resource); + context.addRoutes(builder); + } else { + PluginHelper.getRoutesLoader(context).loadRoutes(resource); } + assertThat(SemanticQuestions.get(context).isEmpty()).isFalse(); + Files.delete(file); + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + } + }); + assertThat(SemanticQuestions.get(context).isEmpty()).isTrue(); + } + + private void load(String location, String xml) throws Exception { + PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString(location, xml)); } private static class TestWatcher extends RouteWatcherReloadStrategy { diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/beans.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/beans.json index eb9b74f87e8ef..f62e00a438a60 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/beans.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/beans.json @@ -14,14 +14,13 @@ "properties": { "component-scan": { "index": 0, "kind": "element", "displayName": "Component-scan", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Component scanning that can auto-discover Camel route builders from the classpath." }, "bean": { "index": 1, "kind": "element", "displayName": "Bean", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "List of bean definitions to be registered in the Camel registry." }, - "semantic": { "index": 2, "kind": "element", "displayName": "Semantic", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.app.SemanticDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic question declarations." }, - "sslContextParameters": { "index": 3, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, - "dataFormat": { "index": 4, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, - "restConfiguration": { "index": 5, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, - "rest": { "index": 6, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, - "routeConfiguration": { "index": 7, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, - "routeTemplate": { "index": 8, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, - "templatedRoute": { "index": 9, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, - "route": { "index": 10, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } + "sslContextParameters": { "index": 2, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, + "dataFormat": { "index": 3, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, + "restConfiguration": { "index": 4, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, + "rest": { "index": 5, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, + "routeConfiguration": { "index": 6, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, + "routeTemplate": { "index": 7, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, + "templatedRoute": { "index": 8, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, + "route": { "index": 9, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } } } diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/camel.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/camel.json index 94b3cebf509f3..1ac564378c757 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/camel.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/camel.json @@ -14,14 +14,13 @@ "properties": { "component-scan": { "index": 0, "kind": "element", "displayName": "Component-scan", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Component scanning that can auto-discover Camel route builders from the classpath." }, "bean": { "index": 1, "kind": "element", "displayName": "Bean", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "List of bean definitions to be registered in the Camel registry." }, - "semantic": { "index": 2, "kind": "element", "displayName": "Semantic", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.app.SemanticDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic question declarations." }, - "sslContextParameters": { "index": 3, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, - "dataFormat": { "index": 4, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, - "restConfiguration": { "index": 5, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, - "rest": { "index": 6, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, - "routeConfiguration": { "index": 7, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, - "routeTemplate": { "index": 8, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, - "templatedRoute": { "index": 9, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, - "route": { "index": 10, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } + "sslContextParameters": { "index": 2, "kind": "element", "displayName": "Ssl Context Parameters", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "SSL\/TLS context parameters configuration." }, + "dataFormat": { "index": 3, "kind": "element", "displayName": "Data Format", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel data formats." }, + "restConfiguration": { "index": 4, "kind": "element", "displayName": "Rest Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL configuration." }, + "rest": { "index": 5, "kind": "element", "displayName": "Rest", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel Rest DSL services." }, + "routeConfiguration": { "index": 6, "kind": "element", "displayName": "Route Configuration", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route configurations." }, + "routeTemplate": { "index": 7, "kind": "element", "displayName": "Route Template", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel route templates." }, + "templatedRoute": { "index": 8, "kind": "element", "displayName": "Templated Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes to be created from route templates." }, + "route": { "index": 9, "kind": "element", "displayName": "Route", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Camel routes." } } } diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticDefinition.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticDefinition.json deleted file mode 100644 index e66c595edd178..0000000000000 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticDefinition.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "model": { - "kind": "model", - "name": "semanticDefinition", - "title": "Semantic Definition", - "description": "Named, provider-independent semantic question declarations.", - "deprecated": false, - "label": "configuration", - "javaType": "org.apache.camel.model.app.SemanticDefinition", - "abstract": false, - "input": false, - "output": false - }, - "properties": { - "question": { "index": 0, "kind": "element", "displayName": "Question", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic questions shared by routes in this Camel context." } - } -} diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticQuestionDefinition.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticQuestionDefinition.json deleted file mode 100644 index a530faab49795..0000000000000 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/app/semanticQuestionDefinition.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "model": { - "kind": "model", - "name": "semanticQuestionDefinition", - "title": "Semantic Question Definition", - "description": "A semantic question with the same fields and policies as a YAML declaration.", - "deprecated": false, - "label": "configuration", - "javaType": "org.apache.camel.model.app.SemanticQuestionDefinition", - "abstract": false, - "input": false, - "output": false - }, - "properties": { - "name": { "index": 0, "kind": "attribute", "displayName": "Name", "group": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The context-wide question name." }, - "type": { "index": 1, "kind": "attribute", "displayName": "Type", "group": "common", "required": true, "type": "enum", "javaType": "java.lang.String", "enum": [ "boolean", "choice", "score" ], "deprecated": false, "autowired": false, "secret": false, "description": "The question type." }, - "state": { "index": 2, "kind": "attribute", "displayName": "State", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The Simple expression selecting the message state." }, - "threshold": { "index": 3, "kind": "attribute", "displayName": "Threshold", "group": "common", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "0.5", "description": "The boolean decision threshold." }, - "uncertainty": { "index": 4, "kind": "attribute", "displayName": "Uncertainty", "group": "common", "required": false, "type": "number", "javaType": "java.lang.Double", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "0", "description": "The boolean uncertainty band." }, - "uncertaintyPolicy": { "index": 5, "kind": "attribute", "displayName": "Uncertainty Policy", "group": "common", "required": false, "type": "enum", "javaType": "java.lang.String", "enum": [ "fail", "non-match" ], "deprecated": false, "autowired": false, "secret": false, "defaultValue": "fail", "description": "The boolean uncertainty policy." }, - "instructions": { "index": 6, "kind": "element", "displayName": "Instructions", "group": "common", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "Instructions describing the judgment to make." }, - "criterion": { "index": 7, "kind": "element", "displayName": "Criterion", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Named choice criteria, or optional true\/false boolean criteria." }, - "level": { "index": 8, "kind": "element", "displayName": "Level", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "deprecated": false, "autowired": false, "secret": false, "description": "Ordered descriptive levels for a score question." } - } -} diff --git a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/routes.json b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/routes.json index ce9cc06276050..ec9c05fa2baf7 100644 --- a/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/routes.json +++ b/core/camel-core-model/src/generated/resources/META-INF/org/apache/camel/model/routes.json @@ -15,7 +15,6 @@ "id": { "index": 0, "kind": "attribute", "displayName": "Id", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The id of this node" }, "note": { "index": 1, "kind": "attribute", "displayName": "Note", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The note for this node" }, "description": { "index": 2, "kind": "attribute", "displayName": "Description", "group": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The description for this node" }, - "routes": { "index": 3, "kind": "element", "displayName": "Routes", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "oneOf": [ "route" ], "deprecated": false, "autowired": false, "secret": false, "description": "Contains the Camel routes" }, - "semantic": { "index": 4, "kind": "element", "displayName": "Semantic", "group": "common", "required": false, "type": "object", "javaType": "org.apache.camel.model.app.SemanticDefinition", "deprecated": false, "autowired": false, "secret": false, "description": "Named semantic question declarations." } + "routes": { "index": 3, "kind": "element", "displayName": "Routes", "group": "common", "required": false, "type": "array", "javaType": "java.util.List", "oneOf": [ "route" ], "deprecated": false, "autowired": false, "secret": false, "description": "Contains the Camel routes" } } } diff --git a/core/camel-core-model/src/generated/resources/org/apache/camel/model/app/jaxb.index b/core/camel-core-model/src/generated/resources/org/apache/camel/model/app/jaxb.index index 55c247965cd5f..0c1c62edebf95 100644 --- a/core/camel-core-model/src/generated/resources/org/apache/camel/model/app/jaxb.index +++ b/core/camel-core-model/src/generated/resources/org/apache/camel/model/app/jaxb.index @@ -7,5 +7,3 @@ BeanPropertyDefinition BeansDefinition ComponentScanDefinition SSLContextParametersDefinition -SemanticDefinition -SemanticQuestionDefinition diff --git a/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java b/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java index 5e2e23be00651..7110c7642a6f1 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/builder/RouteBuilder.java @@ -46,10 +46,8 @@ import org.apache.camel.model.RouteTemplateDefinition; import org.apache.camel.model.RouteTemplatesDefinition; import org.apache.camel.model.RoutesDefinition; -import org.apache.camel.model.SemanticDefinitionHelper; import org.apache.camel.model.TemplatedRouteDefinition; import org.apache.camel.model.TemplatedRoutesDefinition; -import org.apache.camel.model.app.SemanticDefinition; import org.apache.camel.model.errorhandler.RefErrorHandlerDefinition; import org.apache.camel.model.rest.RestConfigurationDefinition; import org.apache.camel.model.rest.RestDefinition; @@ -201,18 +199,6 @@ public String toString() { */ public abstract void configure() throws Exception; - /** - * Declares named semantic questions for use with the semantic language. Requires camel-semantic at runtime. - * - * @since 4.23 - */ - public SemanticDefinition semanticQuestions() { - if (getRouteCollection().getSemantic() == null) { - getRouteCollection().setSemantic(new SemanticDefinition()); - } - return getRouteCollection().getSemantic(); - } - /** * Called on initialization to build routes configuration (global routes configurations) using the fluent builder * syntax. @@ -924,13 +910,6 @@ protected void checkInitialized() throws Exception { configure(); - if (getResource() != null || getRouteCollection().getSemantic() != null) { - String source = getResource() != null - ? getResource().getLocation() - : "java:" + camelContext.getUuidGenerator().generateUuid(); - SemanticDefinitionHelper.configure(camelContext, getResource(), source, getRouteCollection().getSemantic()); - } - // remember the source resource getRouteCollection().setResource(getResource()); getRestCollection().setResource(getResource()); diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java index b171aa1646991..d8bb7ee32ae07 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/RoutesDefinition.java @@ -23,17 +23,14 @@ import jakarta.xml.bind.annotation.XmlAccessType; import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlElementRef; import jakarta.xml.bind.annotation.XmlRootElement; import jakarta.xml.bind.annotation.XmlTransient; -import jakarta.xml.bind.annotation.XmlType; import org.apache.camel.CamelContext; import org.apache.camel.Endpoint; import org.apache.camel.ErrorHandlerFactory; import org.apache.camel.builder.EndpointConsumerBuilder; -import org.apache.camel.model.app.SemanticDefinition; import org.apache.camel.spi.AsEndpointUri; import org.apache.camel.spi.Metadata; import org.apache.camel.spi.Resource; @@ -51,7 +48,6 @@ */ @Metadata(label = "configuration", description = "Container for a collection of route definitions") @XmlRootElement(name = "routes") -@XmlType(propOrder = { "semantic", "routes" }) @XmlAccessorType(XmlAccessType.FIELD) public class RoutesDefinition extends OptionalIdentifiedDefinition implements RouteContainer, ResourceAware { @@ -79,10 +75,6 @@ public class RoutesDefinition extends OptionalIdentifiedDefinition routes = new ArrayList<>(); - @XmlElement(name = "semantic") - @Metadata(description = "Named semantic question declarations.") - private SemanticDefinition semantic; - public RoutesDefinition() { } @@ -113,14 +105,6 @@ public void setRoutes(List routes) { this.routes = routes; } - public SemanticDefinition getSemantic() { - return semantic; - } - - public void setSemantic(SemanticDefinition semantic) { - this.semantic = semantic; - } - public List getInterceptFroms() { return interceptFroms; } diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java b/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java deleted file mode 100644 index 57ab5253c1e27..0000000000000 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.model; - -import org.apache.camel.CamelContext; -import org.apache.camel.model.app.SemanticDefinition; -import org.apache.camel.model.spi.SemanticDefinitionConfigurer; -import org.apache.camel.spi.Resource; - -/** Connects semantic declarations to the optional semantic language module. */ -public final class SemanticDefinitionHelper { - private SemanticDefinitionHelper() { - } - - /** Register declarations before route initialization, without requiring semantic support for ordinary routes. */ - public static void configure( - CamelContext context, Resource resource, String source, SemanticDefinition definition) { - SemanticDefinitionConfigurer configurer - = context.getCamelContextExtension().getContextPlugin(SemanticDefinitionConfigurer.class); - if (configurer == null) { - if (definition == null || definition.getQuestions().isEmpty()) { - return; - } - configurer = getConfigurer(context); - if (configurer == null) { - throw new IllegalArgumentException("Semantic question declarations require camel-semantic on the classpath"); - } - } - configurer.configure(context, resource, source, definition); - } - - /** Export registered questions without requiring the semantic module for ordinary routes. */ - public static SemanticDefinition getDefinition(CamelContext context) { - // YAML declarations can populate the registry before the configurer has been discovered. - SemanticDefinitionConfigurer configurer = getConfigurer(context); - return configurer != null ? configurer.getDefinition(context) : null; - } - - private static SemanticDefinitionConfigurer getConfigurer(CamelContext context) { - SemanticDefinitionConfigurer configurer - = context.getCamelContextExtension().getContextPlugin(SemanticDefinitionConfigurer.class); - if (configurer == null) { - // Concurrent discovery may create equivalent instances of the stateless default configurer. - // The semantic module synchronizes access to shared question state. - configurer = context.getCamelContextExtension().getDefaultFactoryFinder() - .newInstance("semantic-configurer", SemanticDefinitionConfigurer.class).orElse(null); - if (configurer != null) { - context.getCamelContextExtension().addContextPlugin(SemanticDefinitionConfigurer.class, configurer); - } - } - return configurer; - } -} diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/BeansDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/app/BeansDefinition.java index 6065b1d1fdb6c..6643d11f6eb34 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/app/BeansDefinition.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/app/BeansDefinition.java @@ -54,7 +54,6 @@ @XmlType(propOrder = { "componentScanning", "beans", - "semantic", "springOrBlueprintBeans", "sslContextParameters", "dataFormats", @@ -80,18 +79,6 @@ public class BeansDefinition { @Metadata(description = "List of bean definitions to be registered in the Camel registry.") private List beans = new ArrayList<>(); - @XmlElement(name = "semantic") - @Metadata(description = "Named semantic question declarations.") - private SemanticDefinition semantic; - - public SemanticDefinition getSemantic() { - return semantic; - } - - public void setSemantic(SemanticDefinition semantic) { - this.semantic = semantic; - } - // support for legacy spring and blueprint files to be parsed and loaded // for migration and tooling effort (need to be in a single @XmlAnyElement as otherwise // this causes camel-spring-xml to generate an invalid XSD diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java deleted file mode 100644 index ae3fe8a55e8e8..0000000000000 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticDefinition.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.model.app; - -import java.util.ArrayList; -import java.util.List; - -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlType; - -import org.apache.camel.spi.Metadata; - -/** Named, provider-independent semantic question declarations. */ -@Metadata(label = "configuration") -@XmlType(name = "semanticDefinition") -@XmlAccessorType(XmlAccessType.FIELD) -public class SemanticDefinition { - @XmlElement(name = "question") - @Metadata(description = "Named semantic questions shared by routes in this Camel context.") - private List questions = new ArrayList<>(); - - public List getQuestions() { - return questions; - } - - public void setQuestions(List questions) { - this.questions = questions; - } - - /** Declare a named semantic question. */ - public SemanticQuestionDefinition question(String name) { - SemanticQuestionDefinition question = new SemanticQuestionDefinition(); - question.setName(name); - questions.add(question); - return question; - } -} diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticQuestionDefinition.java b/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticQuestionDefinition.java deleted file mode 100644 index 15f4f6787c653..0000000000000 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/app/SemanticQuestionDefinition.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * 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.model.app; - -import java.util.ArrayList; -import java.util.List; - -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlAttribute; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlType; - -import org.apache.camel.model.PropertyDefinition; -import org.apache.camel.spi.Metadata; - -/** A semantic question with the same fields and policies as a YAML declaration. */ -@Metadata(label = "configuration") -@XmlType(name = "semanticQuestionDefinition", propOrder = { "instructions", "criteria", "levels" }) -@XmlAccessorType(XmlAccessType.FIELD) -public class SemanticQuestionDefinition { - @XmlAttribute(required = true) - @Metadata(required = true, description = "The context-wide question name.") - private String name; - @XmlAttribute(required = true) - @Metadata(required = true, enums = "boolean,choice,score", description = "The question type.") - private String type; - @XmlAttribute - @Metadata(description = "The Simple expression selecting the message state.") - private String state; - @XmlAttribute - @Metadata(javaType = "java.lang.Double", defaultValue = "0.5", description = "The boolean decision threshold.") - private String threshold; - @XmlAttribute - @Metadata(javaType = "java.lang.Double", defaultValue = "0", description = "The boolean uncertainty band.") - private String uncertainty; - @XmlAttribute - @Metadata(defaultValue = "fail", enums = "fail,non-match", description = "The boolean uncertainty policy.") - private String uncertaintyPolicy; - @XmlElement(required = true) - @Metadata(required = true, description = "Instructions describing the judgment to make.") - private String instructions; - @XmlElement(name = "criterion") - @Metadata(description = "Named choice criteria, or optional true/false boolean criteria.") - private List criteria = new ArrayList<>(); - @XmlElement(name = "level") - @Metadata(description = "Ordered descriptive levels for a score question.") - private List levels = new ArrayList<>(); - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getState() { - return state; - } - - public void setState(String state) { - this.state = state; - } - - public String getThreshold() { - return threshold; - } - - public void setThreshold(String threshold) { - this.threshold = threshold; - } - - public String getUncertainty() { - return uncertainty; - } - - public void setUncertainty(String uncertainty) { - this.uncertainty = uncertainty; - } - - public String getUncertaintyPolicy() { - return uncertaintyPolicy; - } - - public void setUncertaintyPolicy(String uncertaintyPolicy) { - this.uncertaintyPolicy = uncertaintyPolicy; - } - - public String getInstructions() { - return instructions; - } - - public void setInstructions(String instructions) { - this.instructions = instructions; - } - - public List getCriteria() { - return criteria; - } - - public void setCriteria(List criteria) { - this.criteria = criteria; - } - - public List getLevels() { - return levels; - } - - public void setLevels(List levels) { - this.levels = levels; - } - - /** The question type. */ - public SemanticQuestionDefinition type(String type) { - this.type = type; - return this; - } - - /** The Simple expression selecting the message state. */ - public SemanticQuestionDefinition state(String state) { - this.state = state; - return this; - } - - /** The boolean decision threshold. */ - public SemanticQuestionDefinition threshold(double threshold) { - return threshold(Double.toString(threshold)); - } - - /** The boolean decision threshold, allowing property placeholders. */ - public SemanticQuestionDefinition threshold(String threshold) { - this.threshold = threshold; - return this; - } - - /** The boolean uncertainty band. */ - public SemanticQuestionDefinition uncertainty(double uncertainty) { - return uncertainty(Double.toString(uncertainty)); - } - - /** The boolean uncertainty band, allowing property placeholders. */ - public SemanticQuestionDefinition uncertainty(String uncertainty) { - this.uncertainty = uncertainty; - return this; - } - - /** The boolean uncertainty policy. */ - public SemanticQuestionDefinition uncertaintyPolicy(String uncertaintyPolicy) { - this.uncertaintyPolicy = uncertaintyPolicy; - return this; - } - - /** Instructions describing the judgment to make. */ - public SemanticQuestionDefinition instructions(String instructions) { - this.instructions = instructions; - return this; - } - - /** Add a named criterion. */ - public SemanticQuestionDefinition criterion(String name, String description) { - criteria.add(new PropertyDefinition(name, description)); - return this; - } - - /** Add the next descriptive score level. */ - public SemanticQuestionDefinition level(String description) { - levels.add(description); - return this; - } -} diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java b/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java deleted file mode 100644 index 38db8263f6b57..0000000000000 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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.model.spi; - -import org.apache.camel.CamelContext; -import org.apache.camel.model.app.SemanticDefinition; -import org.apache.camel.spi.Resource; - -/** Installs semantic declarations using the optional semantic language module. */ -public interface SemanticDefinitionConfigurer { - /** - * Replace the questions belonging to this source, including removing obsolete declarations. The source identifies - * the owner independently of the optional resource used to track deletion. - */ - void configure(CamelContext context, Resource resource, String source, SemanticDefinition definition); - - /** Export registered questions with their effective policies, or {@code null} if no definitions are available. */ - default SemanticDefinition getDefinition(CamelContext context) { - return null; - } -} diff --git a/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java b/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java index 6fd7e0ef94561..6bd836608b24d 100644 --- a/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java +++ b/core/camel-java-io/src/generated/java/org/apache/camel/java/out/JavaDslModelWriter.java @@ -2473,7 +2473,6 @@ protected void doWriteRouteTemplatesDefinition(StringBuilder sb, RouteTemplatesD } protected void doWriteRoutesDefinition(StringBuilder sb, RoutesDefinition def) { doWriteOptionalIdentifiedDefinitionAttributes(sb, def); - doWriteChildElement(sb, "semantic", def.getSemantic(), this::doWriteSemanticDefinition); doWriteChildList(sb, "routes", def.getRoutes(), this::doWriteRouteDefinitionRef); } protected void doWriteRoutingSlipDefinition(StringBuilder sb, RoutingSlipDefinition def) { @@ -2789,7 +2788,6 @@ protected void doWriteBeanPropertyDefinition(StringBuilder sb, BeanPropertyDefin protected void doWriteBeansDefinitionElements(StringBuilder sb, BeansDefinition def) { doWriteChildList(sb, "component-scan", def.getComponentScanning(), this::doWriteComponentScanDefinition); doWriteChildList(sb, "bean", def.getBeans(), this::doWriteBeanFactoryDefinition); - doWriteChildElement(sb, "semantic", def.getSemantic(), this::doWriteSemanticDefinition); // @XmlAnyElement - not applicable for Java DSL doWriteChildList(sb, "sslContextParameters", def.getSslContextParameters(), this::doWriteSSLContextParametersDefinition); doWriteChildList(sb, "dataFormat", def.getDataFormats(), this::doWriteDataFormatDefinition); diff --git a/core/camel-java-io/src/main/java/org/apache/camel/java/LwModelToJavaDumper.java b/core/camel-java-io/src/main/java/org/apache/camel/java/LwModelToJavaDumper.java index b13b7283d7d9a..a31ffe45207eb 100644 --- a/core/camel-java-io/src/main/java/org/apache/camel/java/LwModelToJavaDumper.java +++ b/core/camel-java-io/src/main/java/org/apache/camel/java/LwModelToJavaDumper.java @@ -94,9 +94,6 @@ public String dumpModelAsJava( StringBuilder sb = new StringBuilder(); try { if (definition instanceof RoutesDefinition rd) { - if (rd.getSemantic() != null) { - sb.append(writer.writeSemanticDefinition(rd.getSemantic())); - } for (RouteDefinition route : rd.getRoutes()) { if (!sb.isEmpty()) { sb.append("\n\n"); diff --git a/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java b/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java index fa651e1963c87..f54e1dcd2aa5b 100644 --- a/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java +++ b/core/camel-java-io/src/main/java/org/apache/camel/java/out/JavaDslModelWriterSupport.java @@ -43,8 +43,6 @@ import org.apache.camel.model.TemplatedRouteParameterDefinition; import org.apache.camel.model.ToDefinition; import org.apache.camel.model.ValueDefinition; -import org.apache.camel.model.app.SemanticDefinition; -import org.apache.camel.model.app.SemanticQuestionDefinition; import org.apache.camel.model.config.BatchResequencerConfig; import org.apache.camel.model.errorhandler.DeadLetterChannelDefinition; import org.apache.camel.model.errorhandler.DefaultErrorHandlerDefinition; @@ -1133,43 +1131,6 @@ protected String indent() { return " ".repeat(indentLevel); } - public String writeSemanticDefinition(SemanticDefinition definition) { - StringBuilder result = new StringBuilder(); - doWriteSemanticDefinition(result, definition); - return result.toString(); - } - - protected void doWriteSemanticDefinition(StringBuilder result, SemanticDefinition definition) { - for (SemanticQuestionDefinition question : definition.getQuestions()) { - if (!result.isEmpty()) { - result.append(NL); - } - result.append("semanticQuestions().question(").append(quote(question.getName())).append(")") - .append(".type(").append(quote(question.getType())).append(")") - .append(".instructions(").append(quote(question.getInstructions())).append(")"); - if (question.getState() != null) { - result.append(".state(").append(quote(question.getState())).append(")"); - } - if (question.getThreshold() != null) { - result.append(".threshold(").append(quote(question.getThreshold())).append(")"); - } - if (question.getUncertainty() != null) { - result.append(".uncertainty(").append(quote(question.getUncertainty())).append(")"); - } - if (question.getUncertaintyPolicy() != null) { - result.append(".uncertaintyPolicy(").append(quote(question.getUncertaintyPolicy())).append(")"); - } - for (PropertyDefinition criterion : question.getCriteria()) { - result.append(".criterion(").append(quote(criterion.getKey())).append(", ") - .append(quote(criterion.getValue())).append(")"); - } - for (String level : question.getLevels()) { - result.append(".level(").append(quote(level)).append(")"); - } - result.append(";"); - } - } - protected String quote(String s) { if (s == null) { return "null"; diff --git a/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java b/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java index ab3fc4fad7b8e..edb798a0291f9 100644 --- a/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java +++ b/core/camel-java-io/src/test/java/org/apache/camel/java/out/JavaDslCompileTest.java @@ -38,7 +38,6 @@ import javax.tools.SimpleJavaFileObject; import javax.tools.ToolProvider; -import org.apache.camel.java.LwModelToJavaDumper; import org.apache.camel.model.RouteConfigurationDefinition; import org.apache.camel.model.RouteConfigurationsDefinition; import org.apache.camel.model.RouteDefinition; @@ -133,31 +132,6 @@ private static void collectXmlRouteFiles(Path dir, List result) throws E } } - @Test - void semanticDeclarationsCompileWithTheirRoutes() throws Exception { - try (var input = Files.newInputStream(XML_IO_RESOURCES.resolve("semantic-routes.xml"))) { - RoutesDefinition routes = new ModelParser(input, NAMESPACE).parseRoutesDefinition().orElseThrow(); - String java = new LwModelToJavaDumper().dumpModelAsJava(null, routes); - assertTrue(java.contains("semanticQuestions().question(\"department\")")); - assertTrue(java.contains(".threshold(\"0.8\")")); - List errors = compile("SemanticRoutes", wrapInRouteBuilder("SemanticRoutes", List.of(java))); - assertTrue(errors.isEmpty(), () -> errors + "\n" + java); - } - } - - @Test - void semanticNumericPlaceholdersSurviveJavaExport() throws Exception { - try (var input = Files.newInputStream(XML_IO_RESOURCES.resolve("semantic-routes.xml"))) { - RoutesDefinition routes = new ModelParser(input, NAMESPACE).parseRoutesDefinition().orElseThrow(); - routes.getSemantic().getQuestions().get(0).threshold("{{threshold}}").uncertainty("{{uncertainty}}"); - String java = new LwModelToJavaDumper().dumpModelAsJava(null, routes); - assertTrue(java.contains(".threshold(\"{{threshold}}\")")); - assertTrue(java.contains(".uncertainty(\"{{uncertainty}}\")")); - List errors = compile("SemanticPlaceholders", wrapInRouteBuilder("SemanticPlaceholders", List.of(java))); - assertTrue(errors.isEmpty(), () -> errors + "\n" + java); - } - } - @ParameterizedTest(name = "{0}") @MethodSource("xmlRouteFiles") void xmlToJavaDslCompiles(String xmlFile) throws Exception { diff --git a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java index b9f198f7f47b6..10f7d4edf7366 100644 --- a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java +++ b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/in/ModelParser.java @@ -1049,7 +1049,6 @@ private Optional parseSingleRouteTemplatesDefinition() protected RoutesDefinition doParseRoutesDefinition() throws IOException, XmlPullParserException { return doParse(new RoutesDefinition(), optionalIdentifiedDefinitionAttributeHandler(), (def, key) -> switch (key) { case "route": doAdd(doParseRouteDefinition(), def.getRoutes(), def::setRoutes); yield true; - case "semantic": def.setSemantic(doParseSemanticDefinition()); yield true; default: yield optionalIdentifiedDefinitionElementHandler().accept(def, key); }, noValueHandler()); } @@ -1074,12 +1073,6 @@ private Optional parseSingleRoutesDefinition() throws IOExcept } return Optional.empty(); } - protected SemanticDefinition doParseSemanticDefinition() throws IOException, XmlPullParserException { - return doParse(new SemanticDefinition(), noAttributeHandler(), (def, key) -> switch (key) { - case "question": doAdd(doParseSemanticQuestionDefinition(), def.getQuestions(), def::setQuestions); yield true; - default: yield false; - }, noValueHandler()); - } protected RoutingSlipDefinition doParseRoutingSlipDefinition() throws IOException, XmlPullParserException { return doParse(new RoutingSlipDefinition(), (def, key, val) -> switch (key) { case "allowedSchemes": def.setAllowedSchemes(val); yield true; @@ -1437,7 +1430,6 @@ protected ElementHandler beansDefinitionElementHa case "routeConfiguration": doAdd(doParseRouteConfigurationDefinition(), def.getRouteConfigurations(), def::setRouteConfigurations); break; case "routeTemplate": doAdd(doParseRouteTemplateDefinition(), def.getRouteTemplates(), def::setRouteTemplates); break; case "route": doAdd(doParseRouteDefinition(), def.getRoutes(), def::setRoutes); break; - case "semantic": def.setSemantic(doParseSemanticDefinition()); break; case "sslContextParameters": doAdd(doParseSSLContextParametersDefinition(), def.getSslContextParameters(), def::setSslContextParameters); break; case "templatedRoute": doAdd(doParseTemplatedRouteDefinition(), def.getTemplatedRoutes(), def::setTemplatedRoutes); break; default: @@ -1553,22 +1545,6 @@ protected BeanPropertyDefinition doParseBeanPropertyDefinition() throws IOExcept default: yield false; }, noValueHandler()); } - protected SemanticQuestionDefinition doParseSemanticQuestionDefinition() throws IOException, XmlPullParserException { - return doParse(new SemanticQuestionDefinition(), (def, key, val) -> switch (key) { - case "name": def.setName(val); yield true; - case "state": def.setState(val); yield true; - case "threshold": def.setThreshold(val); yield true; - case "type": def.setType(val); yield true; - case "uncertainty": def.setUncertainty(val); yield true; - case "uncertaintyPolicy": def.setUncertaintyPolicy(val); yield true; - default: yield false; - }, (def, key) -> switch (key) { - case "criterion": doAdd(doParsePropertyDefinition(), def.getCriteria(), def::setCriteria); yield true; - case "instructions": def.setInstructions(doParseText()); yield true; - case "level": doAdd(doParseText(), def.getLevels(), def::setLevels); yield true; - default: yield false; - }, noValueHandler()); - } protected BatchResequencerConfig doParseBatchResequencerConfig() throws IOException, XmlPullParserException { return doParse(new BatchResequencerConfig(), (def, key, val) -> switch (key) { case "allowDuplicates": def.setAllowDuplicates(val); yield true; diff --git a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java index d976097922d7b..cf985566ea488 100644 --- a/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java +++ b/core/camel-xml-io/src/generated/java/org/apache/camel/xml/out/ModelWriter.java @@ -1684,7 +1684,6 @@ protected void doWriteRouteTemplatesDefinition(String name, RouteTemplatesDefini protected void doWriteRoutesDefinition(String name, RoutesDefinition def) throws IOException { startElement(name); doWriteOptionalIdentifiedDefinitionAttributes(def); - doWriteElement("semantic", def.getSemantic(), this::doWriteSemanticDefinition); doWriteList(null, null, def.getRoutes(), this::doWriteRouteDefinitionRef); endElement(name); } @@ -2083,7 +2082,6 @@ protected void doWriteBeanPropertyDefinition(String name, BeanPropertyDefinition protected void doWriteBeansDefinitionElements(BeansDefinition def) throws IOException { doWriteList(null, "component-scan", def.getComponentScanning(), this::doWriteComponentScanDefinition); doWriteList(null, "bean", def.getBeans(), this::doWriteBeanFactoryDefinition); - doWriteElement("semantic", def.getSemantic(), this::doWriteSemanticDefinition); domElements(def.getSpringOrBlueprintBeans()); doWriteList(null, "sslContextParameters", def.getSslContextParameters(), this::doWriteSSLContextParametersDefinition); doWriteList("dataFormats", "dataFormat", def.getDataFormats(), this::doWriteDataFormatDefinition); @@ -2134,24 +2132,6 @@ protected void doWriteSSLContextParametersDefinition(String name, SSLContextPara doWriteAttribute("clientAuthentication", def.getClientAuthentication(), "NONE"); endElement(name); } - protected void doWriteSemanticDefinition(String name, SemanticDefinition def) throws IOException { - startElement(name); - doWriteList(null, "question", def.getQuestions(), this::doWriteSemanticQuestionDefinition); - endElement(name); - } - protected void doWriteSemanticQuestionDefinition(String name, SemanticQuestionDefinition def) throws IOException { - startElement(name); - doWriteAttribute("name", def.getName(), null); - doWriteAttribute("type", def.getType(), null); - doWriteAttribute("state", def.getState(), null); - doWriteAttribute("threshold", def.getThreshold(), "0.5"); - doWriteAttribute("uncertainty", def.getUncertainty(), "0"); - doWriteAttribute("uncertaintyPolicy", def.getUncertaintyPolicy(), "fail"); - doWriteElement("instructions", def.getInstructions(), this::doWriteString); - doWriteList(null, "criterion", def.getCriteria(), this::doWritePropertyDefinition); - doWriteList(null, "level", def.getLevels(), this::doWriteString); - endElement(name); - } protected void doWriteBatchResequencerConfig(String name, BatchResequencerConfig def) throws IOException { startElement(name); doWriteAttribute("batchSize", def.getBatchSize(), "100"); diff --git a/core/camel-xml-io/src/test/java/org/apache/camel/xml/out/ModelWriterTest.java b/core/camel-xml-io/src/test/java/org/apache/camel/xml/out/ModelWriterTest.java index 7cc364e282de1..d1f86d704e60b 100644 --- a/core/camel-xml-io/src/test/java/org/apache/camel/xml/out/ModelWriterTest.java +++ b/core/camel-xml-io/src/test/java/org/apache/camel/xml/out/ModelWriterTest.java @@ -26,7 +26,6 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.net.URL; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Collection; @@ -38,11 +37,6 @@ import jakarta.xml.bind.annotation.XmlTransient; -import javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.transform.stream.StreamSource; -import javax.xml.validation.SchemaFactory; - import org.w3c.dom.Element; import org.apache.camel.model.RouteTemplatesDefinition; @@ -57,7 +51,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; import org.xmlunit.assertj3.XmlAssert; import org.xmlunit.diff.DefaultNodeMatcher; import org.xmlunit.diff.ElementSelectors; @@ -75,27 +68,6 @@ public class ModelWriterTest { private static final Map TRANSIENT = new ConcurrentHashMap<>(); - @ParameterizedTest - @ValueSource(strings = { "routes", "camel" }) - void semanticDeclarationsBeforeRoutesConformToTheSchema(String root) throws Exception { - URL schema = getClass().getResource("/camel-xml-io.xsd"); - // Clean test builds use the Spring namespace; incremental builds can reuse the packaged xml-io schema. - String namespace = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(schema.toExternalForm()) - .getDocumentElement().getAttribute("targetNamespace"); - assertThat(namespace).isIn("http://camel.apache.org/schema/spring", NAMESPACE); - String xml = """ - <%s xmlns="%s"> - - Valid? - - - - """.formatted(root, namespace, root); - SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) - .newSchema(schema) - .newValidator().validate(new StreamSource(new StringReader(xml))); - } - @ParameterizedTest @MethodSource("routes") @DisplayName("Test xml roundtrip for ") diff --git a/core/camel-xml-io/src/test/resources/beans-semantic.xml b/core/camel-xml-io/src/test/resources/beans-semantic.xml deleted file mode 100644 index dae4972b5c2ca..0000000000000 --- a/core/camel-xml-io/src/test/resources/beans-semantic.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - Is this urgent? - - - - - Which department? - - - - - How urgent? - Routine - Urgent - Critical - - - diff --git a/core/camel-xml-io/src/test/resources/semantic-routes.xml b/core/camel-xml-io/src/test/resources/semantic-routes.xml deleted file mode 100644 index 061c9cda2e9fe..0000000000000 --- a/core/camel-xml-io/src/test/resources/semantic-routes.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - Is this urgent? - - - - - Which department? - - - - - How urgent? - Routine - Urgent - Critical - - - - - ref:department - - diff --git a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java index 2ee4afc192f78..0753ef039cb4c 100644 --- a/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java +++ b/core/camel-yaml-io/src/generated/java/org/apache/camel/yaml/out/YamlModelWriter.java @@ -1680,7 +1680,6 @@ protected JsonObject doWriteRouteTemplatesDefinition(RouteTemplatesDefinition de protected JsonObject doWriteRoutesDefinition(RoutesDefinition def) { JsonObject jo = new JsonObject(); doWriteOptionalIdentifiedDefinitionAttributes(jo, def); - doWriteChildElement(jo, "semantic", def.getSemantic(), this::doWriteSemanticDefinition); doWriteChildList(jo, "routes", "routes", def.getRoutes(), this::doWriteRouteDefinition); return jo; } @@ -2079,7 +2078,6 @@ protected JsonObject doWriteBeanPropertyDefinition(BeanPropertyDefinition def) { protected void doWriteBeansDefinitionElements(JsonObject jo, BeansDefinition def) { doWriteChildList(jo, null, "component-scan", def.getComponentScanning(), this::doWriteComponentScanDefinition); doWriteChildList(jo, null, "bean", def.getBeans(), this::doWriteBeanFactoryDefinition); - doWriteChildElement(jo, "semantic", def.getSemantic(), this::doWriteSemanticDefinition); // @XmlAnyElement - not applicable for YAML doWriteChildList(jo, null, "sslContextParameters", def.getSslContextParameters(), this::doWriteSSLContextParametersDefinition); doWriteChildList(jo, "dataFormats", "dataFormat", def.getDataFormats(), this::doWriteDataFormatDefinition); diff --git a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java index e837a64d7710f..9d4f14b3e024c 100644 --- a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java +++ b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/LwModelToYAMLDumper.java @@ -161,9 +161,6 @@ String resolve(String value, Properties properties) { List roots = new ArrayList<>(); try { if (definition instanceof RoutesDefinition rd) { - if (rd.getSemantic() != null) { - roots.add(writer.writeSemanticDefinition(rd.getSemantic())); - } for (RouteDefinition route : rd.getRoutes()) { roots.add(writer.writeRouteDefinition(route)); } diff --git a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java index d3a10a979e3da..4fe49a1272052 100644 --- a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java +++ b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java @@ -25,8 +25,6 @@ import org.apache.camel.CamelContext; import org.apache.camel.catalog.RuntimeCamelCatalog; -import org.apache.camel.model.app.SemanticDefinition; -import org.apache.camel.model.app.SemanticQuestionDefinition; import org.apache.camel.util.URISupport; import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; @@ -53,44 +51,6 @@ public void setCamelContext(CamelContext camelContext) { this.camelContext = camelContext; } - public JsonObject writeSemanticDefinition(SemanticDefinition definition) { - return wrapNode("semantic", doWriteSemanticDefinition(definition)); - } - - protected JsonObject doWriteSemanticDefinition(SemanticDefinition definition) { - JsonObject questions = new JsonObject(); - for (SemanticQuestionDefinition question : definition.getQuestions()) { - JsonObject value = new JsonObject(); - value.put("type", question.getType()); - value.put("instructions", question.getInstructions()); - if (question.getState() != null) { - value.put("state", question.getState()); - } - doWriteAttribute(value, "threshold", question.getThreshold(), null); - doWriteAttribute(value, "uncertainty", question.getUncertainty(), null); - doWriteAttribute(value, "uncertaintyPolicy", question.getUncertaintyPolicy(), null); - if (!question.getLevels().isEmpty()) { - JsonArray levels = new JsonArray(); - levels.addAll(question.getLevels()); - value.put("criteria", levels); - } else if (!question.getCriteria().isEmpty()) { - JsonObject criteria = new JsonObject(); - question.getCriteria().forEach(criterion -> { - if (criteria.putIfAbsent(criterion.getKey(), criterion.getValue()) != null) { - throw new IllegalArgumentException("Duplicate semantic criterion: " + criterion.getKey()); - } - }); - value.put("criteria", criteria); - } - if (questions.putIfAbsent(question.getName(), value) != null) { - throw new IllegalArgumentException("Duplicate semantic question: " + question.getName()); - } - } - JsonObject result = new JsonObject(); - result.put("question", questions); - return result; - } - protected void doWriteAttribute(JsonObject jo, String key, String value, String defaultValue) { if (value != null && (defaultValue == null || !defaultValue.equals(value))) { jo.put(key, parseValue(value)); diff --git a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlWriterEdgeCasesTest.java b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlWriterEdgeCasesTest.java index de449b3898662..7ce72aa8a0ebc 100644 --- a/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlWriterEdgeCasesTest.java +++ b/core/camel-yaml-io/src/test/java/org/apache/camel/yaml/out/YamlWriterEdgeCasesTest.java @@ -23,48 +23,15 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import org.apache.camel.model.RouteDefinition; -import org.apache.camel.model.app.SemanticDefinition; import org.apache.camel.model.dataformat.JsonDataFormat; import org.apache.camel.util.json.JsonObject; import org.apache.camel.yaml.LwModelToYAMLDumper; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; public class YamlWriterEdgeCasesTest { - @Test - public void semanticExportRejectsDuplicatesInsteadOfDiscardingThem() { - SemanticDefinition questions = new SemanticDefinition(); - questions.question("q").type("boolean").instructions("First?"); - questions.question("q").type("boolean").instructions("Second?"); - YamlModelWriter writer = new YamlModelWriter(); - assertThatThrownBy(() -> writer.writeSemanticDefinition(questions)).hasMessageContaining("Duplicate semantic question"); - - SemanticDefinition criteria = new SemanticDefinition(); - criteria.question("q").type("choice").instructions("Which?").criterion("a", "First").criterion("a", "Second"); - assertThatThrownBy(() -> writer.writeSemanticDefinition(criteria)).hasMessageContaining("Duplicate semantic criterion"); - } - - @Test - public void semanticDeclarationsRetainYamlShapeAndTextTypes() throws Exception { - SemanticDefinition semantic = new SemanticDefinition(); - semantic.question("007").type("boolean").instructions("true").state("42").threshold(0.8) - .uncertainty(0.1).uncertaintyPolicy("non-match").criterion("true", "yes"); - semantic.question("priority").type("score").instructions("Priority?").level("Low").level("High"); - YamlModelWriter writer = new YamlModelWriter(); - String yaml = writer.printAsYaml(List.of(writer.writeSemanticDefinition(semantic))); - JsonNode node = new YAMLMapper().readTree(yaml); - JsonNode questions = node.get(0).get("semantic").get("question"); - assertThat(questions.isObject()).isTrue(); - assertThat(questions.get("007").get("instructions").isTextual()).isTrue(); - assertThat(questions.get("007").get("state").isTextual()).isTrue(); - assertThat(questions.get("007").get("criteria").get("true").asText()).isEqualTo("yes"); - assertThat(questions.get("007").get("threshold").doubleValue()).isEqualTo(0.8); - assertThat(questions.get("priority").get("criteria").get(1).asText()).isEqualTo("High"); - } - @Test public void testTextThatLooksLikeNumber() throws Exception { RouteDefinition route = new RouteDefinition(); diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java index db658a0f65a31..8ab79c7e752c9 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java @@ -34,8 +34,7 @@ import org.apache.camel.model.ProcessorDefinitionHelper; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.RoutesDefinition; -import org.apache.camel.model.SemanticDefinitionHelper; -import org.apache.camel.model.app.SemanticDefinition; +import org.apache.camel.semantic.SemanticQuestions; import org.apache.camel.spi.Resource; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.ResourceHelper; @@ -136,9 +135,6 @@ private String transformXmlToYaml(String xml) throws Exception { YamlModelWriter writer = new YamlModelWriter(); List roots = new ArrayList<>(); - if (routes.getSemantic() != null) { - roots.add(writer.writeSemanticDefinition(routes.getSemantic())); - } for (RouteDefinition route : routes.getRoutes()) { roots.add(writer.writeRouteDefinition(route)); } @@ -164,7 +160,7 @@ private String transformYamlToXml(String yaml) throws Exception { RoutesDefinition rd = new RoutesDefinition(); rd.setRoutes(routeDefs); - rd.setSemantic(SemanticDefinitionHelper.getDefinition(ctx)); + requireSeparateDeclarations(ctx); StringWriter sw = new StringWriter(); new org.apache.camel.xml.out.ModelWriter(sw).writeRoutesDefinition(rd); @@ -174,6 +170,15 @@ private String transformYamlToXml(String yaml) throws Exception { } } + private static void requireSeparateDeclarations(DefaultCamelContext context) { + SemanticQuestions questions = context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class); + if (questions != null && !questions.isEmpty()) { + throw new IllegalArgumentException( + "Semantic declarations cannot be exported by the generic route converter. " + + "Keep them in a separate declaration resource and convert only the routes."); + } + } + private String transformJavaToFormat(String java, String targetFormat) throws Exception { DefaultCamelContext ctx = new DefaultCamelContext(); try { @@ -197,10 +202,7 @@ private String transformJavaToFormat(String java, String targetFormat) throws Ex if ("yaml".equals(targetFormat)) { YamlModelWriter writer = new YamlModelWriter(); List roots = new ArrayList<>(); - SemanticDefinition semantic = SemanticDefinitionHelper.getDefinition(ctx); - if (semantic != null) { - roots.add(writer.writeSemanticDefinition(semantic)); - } + requireSeparateDeclarations(ctx); for (RouteDefinition route : routeDefs) { roots.add(writer.writeRouteDefinition(route)); } @@ -208,7 +210,7 @@ private String transformJavaToFormat(String java, String targetFormat) throws Ex } else { RoutesDefinition rd = new RoutesDefinition(); rd.setRoutes(routeDefs); - rd.setSemantic(SemanticDefinitionHelper.getDefinition(ctx)); + requireSeparateDeclarations(ctx); StringWriter sw = new StringWriter(); new org.apache.camel.xml.out.ModelWriter(sw).writeRoutesDefinition(rd); diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java index 36170287d7200..ee2b90c181be5 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java @@ -20,7 +20,6 @@ import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.model.FilterDefinition; import org.apache.camel.model.SetBodyDefinition; -import org.apache.camel.semantic.SemanticQuestions; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.ResourceHelper; import org.junit.jupiter.api.Test; @@ -57,99 +56,17 @@ void transformXmlToYaml() { } @Test - void transformXmlToYamlPreservesSemanticDeclarations() { - var result = createTools().camel_transform_route( - """ - - Urgent? - ref:urgent - - """, - "xml", "yaml"); - assertThat(result.supported).isTrue(); - assertThat(result.result).contains("semantic:", "question:", "urgent:", "Urgent?", "ref:urgent"); - } - - @ParameterizedTest - @CsvSource({ "yaml,xml", "java,xml", "java,yaml" }) - void semanticDeclarationsSurviveConversionAndReload(String source, String target) throws Exception { - String yaml = """ - - semantic: - question: - urgent: - type: boolean - instructions: Urgent? - threshold: "{{semantic.export.threshold:0.8}}" - uncertainty: "{{semantic.export.uncertainty:0.1}}" - uncertaintyPolicy: non-match - department: - type: choice - state: ${header.selected} - instructions: Which department? - criteria: - billing: Invoices - technical: Bugs - priority: - type: score - instructions: Priority? - criteria: [Low, High] - - route: - from: - uri: direct:input - steps: - - setBody: - expression: - language: - language: semantic - expression: ref:department - """; - String java = """ - import org.apache.camel.builder.RouteBuilder; - public class SemanticRoute extends RouteBuilder { - public void configure() { - semanticQuestions().question("urgent").type("boolean").instructions("Urgent?") - .threshold("{{semantic.export.threshold:0.8}}") - .uncertainty("{{semantic.export.uncertainty:0.1}}").uncertaintyPolicy("non-match"); - semanticQuestions().question("department").type("choice").instructions("Which department?") - .state("${header.selected}").criterion("billing", "Invoices").criterion("technical", "Bugs"); - semanticQuestions().question("priority").type("score").instructions("Priority?") - .level("Low").level("High"); - from("direct:input").setBody().language("semantic", "ref:department"); - } - } - """; - var result = createTools().camel_transform_route(source.equals("java") ? java : yaml, source, target); - assertThat(result.supported).isTrue(); - assertThat(result.result).contains("ref:department").doesNotContain("{{semantic.export."); - try (var context = new DefaultCamelContext()) { - context.build(); - PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("result." + target, result.result)); - var questions = SemanticQuestions.get(context); - assertThat(questions.get("urgent").getThreshold()).isEqualTo(0.8); - assertThat(questions.get("urgent").getUncertainty()).isEqualTo(0.1); - assertThat(questions.get("urgent").getUncertaintyPolicy().name()).isEqualTo("NON_MATCH"); - assertThat(questions.get("department").getCriteria()).containsEntry("billing", "Invoices") - .containsEntry("technical", "Bugs"); - assertThat(questions.get("department").getState()).isEqualTo("${header.selected}"); - assertThat(questions.get("priority").getLevels()).containsExactly("Low", "High"); - } + void genericXmlConversionRejectsComponentDeclarations() { + assertThatThrownBy(() -> createTools().camel_transform_route( + "", "xml", "yaml")) + .isInstanceOf(ToolCallException.class).hasMessageContaining("semantic"); } @ParameterizedTest @CsvSource({ "yaml,xml", "java,xml", "java,yaml" }) - void semanticConversionOmitsDefaultPolicies(String source, String target) throws Exception { - var result = createTools().camel_transform_route(semanticRoute(source, "0.5"), source, target); - - assertThat(result.supported).isTrue(); - assertThat(result.result).contains("urgent", "Urgent?").doesNotContain("threshold", "uncertainty"); - try (var context = new DefaultCamelContext()) { - context.build(); - PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("result." + target, result.result)); - var question = SemanticQuestions.get(context).get("urgent"); - assertThat(question.getThreshold()).isEqualTo(0.5); - assertThat(question.getUncertainty()).isZero(); - assertThat(question.getUncertaintyPolicy().name()).isEqualTo("FAIL"); - } + void genericConversionDoesNotSilentlyLoseSemanticDeclarations(String source, String target) { + assertThatThrownBy(() -> createTools().camel_transform_route(semanticRoute(source, "0.8"), source, target)) + .isInstanceOf(ToolCallException.class).hasMessageContaining("Keep them in a separate declaration resource"); } @ParameterizedTest @@ -165,9 +82,15 @@ void semanticConversionReportsMissingNumericProperty(String source, String targe private static String semanticRoute(String source, String threshold) { if (source.equals("java")) { return """ - semanticQuestions().question("urgent").type("boolean").instructions("Urgent?") - .threshold("%s").uncertainty(0).uncertaintyPolicy("fail"); - from("direct:input").setBody().language("semantic", "ref:urgent"); + import org.apache.camel.builder.RouteBuilder; + import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions; + public class SemanticRoute extends RouteBuilder { + public void configure() { + semanticQuestions(this).question("urgent").type("boolean").instructions("Urgent?") + .threshold("%s").uncertainty(0).uncertaintyPolicy("fail").register(); + from("direct:input").setBody().language("semantic", "ref:urgent"); + } + } """.formatted(threshold); } return """ diff --git a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java index 2319f7c356484..163e88b4501c9 100644 --- a/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java +++ b/dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java @@ -18,7 +18,6 @@ import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -44,12 +43,10 @@ import org.apache.camel.model.RouteTemplateDefinition; import org.apache.camel.model.RouteTemplatesDefinition; import org.apache.camel.model.RoutesDefinition; -import org.apache.camel.model.SemanticDefinitionHelper; import org.apache.camel.model.TemplatedRouteDefinition; import org.apache.camel.model.TemplatedRoutesDefinition; import org.apache.camel.model.app.BeansDefinition; import org.apache.camel.model.app.SSLContextParametersDefinition; -import org.apache.camel.model.app.SemanticDefinition; import org.apache.camel.model.dataformat.DataFormatsDefinition; import org.apache.camel.model.rest.RestConfigurationDefinition; import org.apache.camel.model.rest.RestDefinition; @@ -78,7 +75,6 @@ public class XmlRoutesBuilderLoader extends RouteBuilderLoaderSupport { private final Map resourceCache = new ConcurrentHashMap<>(); private final Map xmlInfoCache = new ConcurrentHashMap<>(); private final Map camelAppCache = new ConcurrentHashMap<>(); - private final Map routesCache = new ConcurrentHashMap<>(); private final List> delayedRegistrations = new ArrayList<>(); private final AtomicInteger counter = new AtomicInteger(0); @@ -96,64 +92,22 @@ public XmlRoutesBuilderLoader() { public void preParseRoute(Resource resource) throws Exception { // preparsing is done at early stage, so we have a chance to load additional beans and populate // Camel registry - try { - // Main may preparse twice. Reuse unchanged input, but refresh edited resources after a failed batch. - Resource snapshot = new CachedResource(resource); - Resource previous = resourceCache.get(resource.getLocation()); - if (preparseDone.getOrDefault(resource.getLocation(), false) && previous != null) { - try (var current = snapshot.getInputStream(); var cached = previous.getInputStream()) { - if (Arrays.equals(current.readAllBytes(), cached.readAllBytes())) { - return; - } - } - } - clearCaches(resource.getLocation()); - resourceCache.put(resource.getLocation(), snapshot); - XmlStreamInfo xmlInfo = xmlInfo(snapshot); - if (xmlInfo.isValid()) { - String root = xmlInfo.getRootElementName(); - SemanticDefinition semantic = null; - if ("beans".equals(root) || "blueprint".equals(root) || "camel".equals(root)) { - new XmlModelParser(snapshot, xmlInfo.getRootElementNamespace()) - .parseBeansDefinition() - .ifPresent(bd -> { - registerBeans(resource, bd); - camelAppCache.put(resource.getLocation(), bd); - }); - BeansDefinition app = camelAppCache.get(resource.getLocation()); - if (app != null) { - semantic = app.getSemantic(); - } - } else if ("routes".equals(root) || "route".equals(root)) { - RoutesDefinition routes = new XmlModelParser(snapshot, xmlInfo.getRootElementNamespace()) - .parseRoutesDefinition().orElse(null); - if (routes != null) { - routesCache.put(resource.getLocation(), routes); - semantic = routes.getSemantic(); - } - } - SemanticDefinitionHelper.configure(getCamelContext(), resource, resource.getLocation(), semantic); + if (preparseDone.getOrDefault(resource.getLocation(), false)) { + return; + } + XmlStreamInfo xmlInfo = xmlInfo(resource); + if (xmlInfo.isValid()) { + String root = xmlInfo.getRootElementName(); + if ("beans".equals(root) || "blueprint".equals(root) || "camel".equals(root)) { + new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) + .parseBeansDefinition() + .ifPresent(bd -> { + registerBeans(resource, bd); + camelAppCache.put(resource.getLocation(), bd); + }); } - preparseDone.put(resource.getLocation(), true); - } catch (Exception e) { - // A failed batch also prevents builders for earlier resources from clearing their cached input. - resourceCache.clear(); - xmlInfoCache.clear(); - camelAppCache.clear(); - routesCache.clear(); - preparseDone.clear(); - delayedRegistrations.clear(); - throw e; } - } - - private void clearCaches(String location) { - resourceCache.remove(location); - xmlInfoCache.remove(location); - camelAppCache.remove(location); - routesCache.remove(location); - preparseDone.remove(location); - delayedRegistrations.removeIf(def -> def.getResource() != null && location.equals(def.getResource().getLocation())); + preparseDone.put(resource.getLocation(), true); } @Override @@ -196,22 +150,19 @@ public void configure() throws Exception { case "rests", "rest" -> new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) .parseRestsDefinition() .ifPresent(this::addRests); - case "routes", "route" -> { - RoutesDefinition routes = routesCache.get(resourceLocation); - if (routes != null) { - addRoutes(routes); - } else { - new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) - .parseRoutesDefinition().ifPresent(this::addRoutes); - } - } + case "routes", "route" -> new XmlModelParser(resource, xmlInfo.getRootElementNamespace()) + .parseRoutesDefinition() + .ifPresent(this::addRoutes); default -> { } } } finally { // knowing this is the last time an XML may have been parsed, we can clear the cache // (route may get reloaded later) - clearCaches(resourceLocation); + resourceCache.remove(resourceLocation); + xmlInfoCache.remove(resourceLocation); + camelAppCache.remove(resourceLocation); + preparseDone.remove(resourceLocation); } } @@ -246,10 +197,10 @@ public void configuration() throws Exception { } private void configureCamel(BeansDefinition app) { - getRouteCollection().setSemantic(app.getSemantic()); if (!delayedRegistrations.isEmpty()) { // some of the beans were not available yet, so we have to try register them now for (BeanFactoryDefinition def : delayedRegistrations) { + def.setResource(getResource()); registerBeanDefinition(def, false); } delayedRegistrations.clear(); @@ -378,9 +329,6 @@ private void addRests(RestsDefinition list) { } private void addRoutes(RoutesDefinition routes) { - if (routes.getSemantic() != null) { - getRouteCollection().setSemantic(routes.getSemantic()); - } // xml routes must be prepared in the same way java-dsl (via RoutesDefinition) // so create a copy and use the fluent builder to add the route for (RouteDefinition route : routes.getRoutes()) { diff --git a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoaderTest.java b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoaderTest.java index 188a9e03e9e2c..0add991d21c60 100644 --- a/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoaderTest.java +++ b/dsl/camel-xml-io-dsl/src/test/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoaderTest.java @@ -20,32 +20,12 @@ import org.apache.camel.builder.RouteConfigurationBuilder; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.spi.Resource; -import org.apache.camel.support.PluginHelper; import org.apache.camel.support.ResourceHelper; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; public class XmlRoutesBuilderLoaderTest { - @Test - public void semanticDeclarationsRequireTheOptionalComponent() throws Exception { - try (DefaultCamelContext context = new DefaultCamelContext()) { - context.start(); - Resource resource = ResourceHelper.fromString("questions.xml", """ - - - Valid? - - - """); - var error = assertThrows(IllegalArgumentException.class, - () -> PluginHelper.getRoutesLoader(context).loadRoutes(resource)); - assertEquals("Semantic question declarations require camel-semantic on the classpath", error.getMessage()); - } - } - @Test public void canLoadRoutes() throws Exception { String content = "" diff --git a/dsl/camel-xml-jaxb-dsl/src/main/java/org/apache/camel/dsl/xml/jaxb/JaxbXmlRoutesBuilderLoader.java b/dsl/camel-xml-jaxb-dsl/src/main/java/org/apache/camel/dsl/xml/jaxb/JaxbXmlRoutesBuilderLoader.java index b27786fa42208..639a8ff0ed821 100644 --- a/dsl/camel-xml-jaxb-dsl/src/main/java/org/apache/camel/dsl/xml/jaxb/JaxbXmlRoutesBuilderLoader.java +++ b/dsl/camel-xml-jaxb-dsl/src/main/java/org/apache/camel/dsl/xml/jaxb/JaxbXmlRoutesBuilderLoader.java @@ -84,7 +84,6 @@ public void configure() throws Exception { try (InputStream is = resourceInputStream(resource)) { RoutesDefinition routes = loadRoutesDefinition(getCamelContext(), is); if (routes != null) { - getRouteCollection().setSemantic(routes.getSemantic()); // xml routes must be prepared in the same way java-dsl (via RoutesDefinition) // so create a copy and use the fluent builder to add the route for (RouteDefinition route : routes.getRoutes()) { diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/pom.xml b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/pom.xml index 6b731bf43035f..1b4947b1ac581 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/pom.xml +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/pom.xml @@ -103,8 +103,6 @@ org.apache.camel.model.RouteConfigurationsDefinition org.apache.camel.model.app.BeansDefinition org.apache.camel.model.app.ApplicationDefinition - org.apache.camel.model.app.SemanticDefinition - org.apache.camel.model.app.SemanticQuestionDefinition diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml b/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml index 7a7e023271f7d..ab1611cab578a 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml @@ -242,11 +242,6 @@ camel-semantic test - - org.apache.camel - camel-yaml-io - test - diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java index 71974099b4d50..646828db2cd4d 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java @@ -29,9 +29,6 @@ import org.apache.camel.dsl.yaml.common.exception.YamlDeserializationException; import org.apache.camel.dsl.yaml.support.YamlTestSupport; import org.apache.camel.language.semantic.SemanticLanguage; -import org.apache.camel.model.RoutesDefinition; -import org.apache.camel.model.app.SemanticDefinition; -import org.apache.camel.model.language.LanguageExpression; import org.apache.camel.semantic.SemanticAdapter; import org.apache.camel.semantic.SemanticQuestion; import org.apache.camel.semantic.SemanticQuestions; @@ -40,7 +37,6 @@ import org.apache.camel.support.PluginHelper; import org.apache.camel.support.ResourceHelper; import org.apache.camel.support.RouteWatcherReloadStrategy; -import org.apache.camel.yaml.LwModelToYAMLDumper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; @@ -48,6 +44,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; +import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -87,38 +84,14 @@ private static String declarations(String state) { """.formatted(state); } - @Test - void exportedModelLoadsWithTheExistingYamlSyntax() throws Exception { - SemanticDefinition semantic = new SemanticDefinition(); - semantic.question("urgent").type("boolean").instructions("Urgent?").threshold(0.8) - .uncertainty(0.1).uncertaintyPolicy("non-match"); - semantic.question("department").type("choice").state("${header.selected}").instructions("Which department?") - .criterion("billing", "Invoices").criterion("technical", "Outages"); - semantic.question("priority").type("score").instructions("Priority?").level("Low").level("High"); - RoutesDefinition routes = new RoutesDefinition(); - routes.setSemantic(semantic); - routes.from("direct:exported").setBody(new LanguageExpression("semantic", "ref:department")); - loadRoutes(new LwModelToYAMLDumper().dumpModelAsYaml(context, routes)); - context.start(); - assertThat(calls).hasValue(0); - var questions = SemanticQuestions.get(context); - assertThat(questions.get("urgent").getThreshold()).isEqualTo(0.8); - assertThat(questions.get("urgent").getUncertaintyPolicy()).isEqualTo(SemanticQuestion.UncertaintyPolicy.NON_MATCH); - assertThat(questions.get("priority").getLevels()).containsExactly("Low", "High"); - try (var template = context.createProducerTemplate()) { - assertThat(template.requestBodyAndHeader("direct:exported", "original", "selected", "invoice")) - .isEqualTo("billing"); - } - } - @Test void javaDeclarationsCoexistWithUnchangedYamlDeclarations() throws Exception { context.addRoutes(new RouteBuilder() { @Override public void configure() { - semanticQuestions().question("javaDepartment").type("choice").state("${header.selected}") + semanticQuestions(this).question("javaDepartment").type("choice").state("${header.selected}") .instructions("Which department?") - .criterion("billing", "Invoices and refunds").criterion("technical", "Bugs and outages"); + .criterion("billing", "Invoices and refunds").criterion("technical", "Bugs and outages").register(); } }); loadRoutes(declarations("${header.selected}") + route()); diff --git a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-java-dsl-writer.vm b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-java-dsl-writer.vm index 6d9a89ca0632a..ac93150b1b3d3 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-java-dsl-writer.vm +++ b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-java-dsl-writer.vm @@ -89,9 +89,7 @@ public class JavaDslModelWriter extends JavaDslModelWriterSupport { #if( $clazz.simpleName == "OptionalIdentifiedDefinition" ) #set( $foo = $elementRefs.add($clazz) ) #end -## Semantic declaration writers are hand-written in JavaDslModelWriterSupport. - #if( !$mojo.getXmlEnum($clazz) && !$clazz.isInterface() - && $clazz.simpleName != "SemanticDefinition" && $clazz.simpleName != "SemanticQuestionDefinition" ) + #if( !$mojo.getXmlEnum($clazz) && !$clazz.isInterface() ) #set( $hasDerived = false ) #foreach( $c in $model ) #if( $c.getSuperclass() == $clazz ) diff --git a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-yaml-writer.vm b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-yaml-writer.vm index 09047eaed9bbf..8b3da44d0e4ce 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-yaml-writer.vm +++ b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/model-yaml-writer.vm @@ -83,9 +83,7 @@ public class YamlModelWriter extends YamlModelWriterSupport { #if( $clazz.simpleName == "OptionalIdentifiedDefinition" ) #set( $foo = $elementRefs.add($clazz) ) #end -## Semantic declaration writers are hand-written in YamlModelWriterSupport. - #if( !$mojo.getXmlEnum($clazz) && !$clazz.isInterface() - && $clazz.simpleName != "SemanticDefinition" && $clazz.simpleName != "SemanticQuestionDefinition" ) + #if( !$mojo.getXmlEnum($clazz) && !$clazz.isInterface() ) #set( $hasDerived = false ) #foreach( $c in $model ) #if( $c.getSuperclass() == $clazz ) From 438d8709b4960f0b8e98c9840c5883f54c086324 Mon Sep 17 00:00:00 2001 From: Luigi De Masi Date: Wed, 30 Sep 2026 10:25:01 +0200 Subject: [PATCH 08/11] CAMEL-25138: Discover semantic declarations in ordinary XML resources Automatically install XML declaration support through the existing context service plugin. Detect declarations or their namespace, delegate ordinary XML to the standard loader, and retain application loader precedence. Preserve resource ownership and manage loader startup, shutdown and restart. Use ordinary XML filenames throughout the documentation and cover both layouts, Camel Main, custom registries, namespace handling and reloads. Co-authored-by: Codex Signed-off-by: Luigi De Masi --- .../camel/catalog/docs/semantic-language.adoc | 25 ++- .../src/main/docs/semantic-language.adoc | 25 ++- .../camel/semantic/SemanticReloadPlugin.java | 58 +++++- .../camel/semantic/SemanticXmlLoader.java | 157 +++++++++++++++ .../SemanticXmlRoutesBuilderLoader.java | 9 +- .../semantic/SemanticDeclarationDslTest.java | 65 ++++--- .../SemanticXmlAutoDiscoveryTest.java | 181 ++++++++++++++++++ .../pages/camel-4x-upgrade-guide-4_23.adoc | 7 + 8 files changed, 480 insertions(+), 47 deletions(-) create mode 100644 components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java create mode 100644 components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java 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 cc9d87d92481b..4f37730d403bf 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 @@ -142,12 +142,14 @@ key and an empty map to `replace` removes the resource's questions. === XML declarations -The optional XML extension belongs to `camel-semantic`. Include `camel-semantic` and -xref:others:java-xml-io-dsl.adoc[XML DSL] (`camel-xml-io-dsl`), and give declaration files the -extension `*.semantic.xml`. This selects the component's loader without changing the standard -XML loader or Camel core model. +The XML extension belongs to `camel-semantic`. Include `camel-semantic` and +xref:others:java-xml-io-dsl.adoc[XML DSL] (`camel-xml-io-dsl`). Camel discovers the extension +automatically at startup: use ordinary `*.xml` files without registering a loader or changing +the Camel core model. XML documents without semantic declarations are handled by the standard +XML loader, including its bean and route configuration support. XML loaders registered by the +application before route loading retain precedence over automatic discovery. -Both layouts are supported. To keep declarations alongside routes, use `tickets.semantic.xml`: +Both layouts are supported. To keep declarations alongside routes, use `tickets.xml`: [source,xml] ---- @@ -170,7 +172,7 @@ Both layouts are supported. To keep declarations alongside routes, use `tickets. ---- -To share declarations across route files, use a standalone `questions.semantic.xml`: +To share declarations across route files, use a standalone `questions.xml`: [source,xml] ---- @@ -186,7 +188,7 @@ To share declarations across route files, use a standalone `questions.semantic.x Load this together with ordinary `*.xml`, Java or YAML route resources that use `ref:department`. For example, with Camel Main, set -`camel.main.routes-include-pattern=classpath:questions.semantic.xml,classpath:routes.xml`. +`camel.main.routes-include-pattern=classpath:questions.xml,classpath:routes.xml`. Declarations are registered before consuming routes are configured. Reload replaces the source's questions; removing the `semantic` block, using an empty block, or deleting the resource removes obsolete definitions. XML source keys are the resource locations. @@ -195,8 +197,13 @@ The combined format supports a `routes` root with one optional `semantic` block `route` elements. The standalone format uses a `semantic` root. Namespace-free documents are supported, as are documents consistently using `http://camel.apache.org/schema/semantic`, `http://camel.apache.org/schema/xml-io`, or `http://camel.apache.org/schema/spring`. -These extensions do not validate against Camel's standard core XSDs. Ordinary `*.xml` files -and the JAXB loader do not accept embedded semantic declarations. +A `semantic` block can also declare `xmlns="http://camel.apache.org/schema/semantic"` inside a +standard `routes` document; its question elements inherit that namespace. The extension detects +the declaration block or semantic root namespace automatically. + +These extensions do not validate against Camel's standard core XSDs. Automatic discovery applies +to Camel's route resource loader; it does not extend Spring's XML application-context parser +or direct JAXB unmarshalling. For boolean questions, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional question attributes. For score questions, replace the named `criterion` elements with ordered `level` 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 cc9d87d92481b..4f37730d403bf 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 @@ -142,12 +142,14 @@ key and an empty map to `replace` removes the resource's questions. === XML declarations -The optional XML extension belongs to `camel-semantic`. Include `camel-semantic` and -xref:others:java-xml-io-dsl.adoc[XML DSL] (`camel-xml-io-dsl`), and give declaration files the -extension `*.semantic.xml`. This selects the component's loader without changing the standard -XML loader or Camel core model. +The XML extension belongs to `camel-semantic`. Include `camel-semantic` and +xref:others:java-xml-io-dsl.adoc[XML DSL] (`camel-xml-io-dsl`). Camel discovers the extension +automatically at startup: use ordinary `*.xml` files without registering a loader or changing +the Camel core model. XML documents without semantic declarations are handled by the standard +XML loader, including its bean and route configuration support. XML loaders registered by the +application before route loading retain precedence over automatic discovery. -Both layouts are supported. To keep declarations alongside routes, use `tickets.semantic.xml`: +Both layouts are supported. To keep declarations alongside routes, use `tickets.xml`: [source,xml] ---- @@ -170,7 +172,7 @@ Both layouts are supported. To keep declarations alongside routes, use `tickets. ---- -To share declarations across route files, use a standalone `questions.semantic.xml`: +To share declarations across route files, use a standalone `questions.xml`: [source,xml] ---- @@ -186,7 +188,7 @@ To share declarations across route files, use a standalone `questions.semantic.x Load this together with ordinary `*.xml`, Java or YAML route resources that use `ref:department`. For example, with Camel Main, set -`camel.main.routes-include-pattern=classpath:questions.semantic.xml,classpath:routes.xml`. +`camel.main.routes-include-pattern=classpath:questions.xml,classpath:routes.xml`. Declarations are registered before consuming routes are configured. Reload replaces the source's questions; removing the `semantic` block, using an empty block, or deleting the resource removes obsolete definitions. XML source keys are the resource locations. @@ -195,8 +197,13 @@ The combined format supports a `routes` root with one optional `semantic` block `route` elements. The standalone format uses a `semantic` root. Namespace-free documents are supported, as are documents consistently using `http://camel.apache.org/schema/semantic`, `http://camel.apache.org/schema/xml-io`, or `http://camel.apache.org/schema/spring`. -These extensions do not validate against Camel's standard core XSDs. Ordinary `*.xml` files -and the JAXB loader do not accept embedded semantic declarations. +A `semantic` block can also declare `xmlns="http://camel.apache.org/schema/semantic"` inside a +standard `routes` document; its question elements inherit that namespace. The extension detects +the declaration block or semantic root namespace automatically. + +These extensions do not validate against Camel's standard core XSDs. Automatic discovery applies +to Camel's route resource loader; it does not extend Spring's XML application-context parser +or direct JAXB unmarshalling. For boolean questions, `threshold`, `uncertainty` and `uncertaintyPolicy` are optional question attributes. For score questions, replace the named `criterion` elements with ordered `level` diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java index 95a4ba3d1342e..1fb38ad3e5020 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java @@ -17,13 +17,67 @@ package org.apache.camel.semantic; import org.apache.camel.CamelContext; +import org.apache.camel.RuntimeCamelException; import org.apache.camel.spi.ContextServicePlugin; +import org.apache.camel.spi.LifecycleStrategy; +import org.apache.camel.spi.RoutesBuilderLoader; +import org.apache.camel.support.LifecycleStrategySupport; +import org.apache.camel.support.service.ServiceHelper; -/** Removes definitions from deleted files before the route watcher loads their replacements. */ +/** Installs optional XML declaration support and removes definitions from deleted files before route reload. */ public class SemanticReloadPlugin implements ContextServicePlugin { + private RoutesBuilderLoader xmlLoader; + private LifecycleStrategy lifecycle; + @Override public void load(CamelContext context) { - // Questions are registered by the route loader or the application. + installXmlLoader(context); + lifecycle = new LifecycleStrategySupport() { + @Override + public void onContextInitializing(CamelContext camelContext) { + // Applications can replace the registry after the context's eager build phase. + installXmlLoader(camelContext); + } + }; + context.addLifecycleStrategy(lifecycle); + } + + private void installXmlLoader(CamelContext context) { + // Keep the language usable without the optional XML/model dependencies, and preserve application overrides. + if (context.getClassResolver().resolveClass("org.apache.camel.xml.in.ModelParser") == null + || context.getRegistry().lookupByName(SemanticXmlLoader.REGISTRY_KEY) != null + || context.getRegistry().findByType(RoutesBuilderLoader.class).stream() + .anyMatch(loader -> loader.isSupportedExtension("xml"))) { + return; + } + if (xmlLoader == null) { + xmlLoader = new SemanticXmlLoader(); + xmlLoader.setCamelContext(context); + } + try { + ServiceHelper.startService(xmlLoader); + context.getRegistry().bind(SemanticXmlLoader.REGISTRY_KEY, xmlLoader); + } catch (Exception e) { + throw RuntimeCamelException.wrapRuntimeException(e); + } + } + + @Override + public void unload(CamelContext context) { + context.getLifecycleStrategies().remove(lifecycle); + lifecycle = null; + if (xmlLoader != null) { + if (context.getRegistry().lookupByName(SemanticXmlLoader.REGISTRY_KEY) == xmlLoader) { + context.getRegistry().unbind(SemanticXmlLoader.REGISTRY_KEY); + } + try { + ServiceHelper.stopAndShutdownService(xmlLoader); + } catch (Exception e) { + throw RuntimeCamelException.wrapRuntimeException(e); + } finally { + xmlLoader = null; + } + } } @Override diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java new file mode 100644 index 0000000000000..dd2d06e2e7236 --- /dev/null +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java @@ -0,0 +1,157 @@ +/* + * 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.semantic; + +import java.io.InputStream; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamReader; + +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.builder.RouteBuilderLifecycleStrategy; +import org.apache.camel.spi.Resource; +import org.apache.camel.spi.RoutesBuilderLoader; +import org.apache.camel.support.CachedResource; +import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.ResolverHelper; +import org.apache.camel.support.RoutesBuilderLoaderSupport; +import org.apache.camel.support.service.ServiceHelper; + +/** Detects semantic declarations in ordinary XML route resources and delegates other XML to the configured loader. */ +final class SemanticXmlLoader extends RoutesBuilderLoaderSupport { + static final String REGISTRY_KEY = "semantic-xml-routes-loader"; + private RoutesBuilderLoader delegate; + private final SemanticXmlRoutesBuilderLoader semantic = new SemanticXmlRoutesBuilderLoader(); + + @Override + public String getSupportedExtension() { + return "xml"; + } + + @Override + public boolean isSupportedExtension(String extension) { + // DefaultCamelContext may build before application beans are bound. Give later custom loaders precedence too. + return ("xml".equals(extension) || "camel.xml".equals(extension)) + && getCamelContext().getRegistry().findByType(RoutesBuilderLoader.class).stream() + .noneMatch(loader -> loader != this && loader.isSupportedExtension(extension)); + } + + @Override + public void preParseRoute(Resource resource) throws Exception { + Resource snapshot = snapshot(resource); + if (hasDeclarations(snapshot)) { + semantic.setCamelContext(getCamelContext()); + semantic.preParseRoute(snapshot); + } else { + delegate().preParseRoute(snapshot); + } + } + + @Override + public RoutesBuilder loadRoutesBuilder(Resource resource) throws Exception { + Resource snapshot = snapshot(resource); + if (hasDeclarations(snapshot)) { + semantic.setCamelContext(getCamelContext()); + return semantic.loadRoutesBuilder(snapshot); + } + RoutesBuilder answer = delegate().loadRoutesBuilder(snapshot); + if (answer instanceof RouteBuilder builder) { + // Declarations removed from a resource are discarded only after its replacement configures successfully. + builder.addLifecycleInterceptor(new RouteBuilderLifecycleStrategy() { + @Override + public void afterConfigure(RouteBuilder routeBuilder) { + SemanticQuestions questions + = getCamelContext().getCamelContextExtension().getContextPlugin(SemanticQuestions.class); + if (questions != null) { + questions.remove(resource.getLocation()); + } + } + }); + } + return answer; + } + + private synchronized RoutesBuilderLoader delegate() throws Exception { + if (delegate == null) { + // Resolve the service directly: looking up "xml" through RoutesLoader would find this wrapper again. + var finder + = getCamelContext().getCamelContextExtension().getBootstrapFactoryFinder(RoutesBuilderLoader.FACTORY_PATH); + RoutesBuilderLoader loader = ResolverHelper + .resolveService(getCamelContext(), finder, "xml", RoutesBuilderLoader.class) + .orElseThrow(() -> new IllegalStateException("An XML routes loader such as camel-xml-io-dsl is required")); + loader.setCamelContext(getCamelContext()); + PluginHelper.getRoutesLoader(getCamelContext()).initRoutesBuilderLoader(loader); + ServiceHelper.startService(loader); + delegate = loader; + } + return delegate; + } + + @Override + protected void doStop() throws Exception { + ServiceHelper.stopAndShutdownServices(delegate, semantic); + delegate = null; + super.doStop(); + } + + private static Resource snapshot(Resource resource) { + // Share bytes only within one call. Preserve the original scheme/existence for deletion tracking. + return new CachedResource(resource) { + @Override + public String getScheme() { + return resource.getScheme(); + } + }; + } + + private static boolean hasDeclarations(Resource resource) throws Exception { + XMLInputFactory factory = XMLInputFactory.newFactory(); + factory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false); + factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false); + try (InputStream stream = resource.getInputStream()) { + XMLStreamReader reader = factory.createXMLStreamReader(stream); + try { + int depth = 0; + while (reader.hasNext()) { + int event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT) { + depth++; + if (depth == 1) { + if ("semantic".equals(reader.getLocalName()) + || SemanticXmlRoutesBuilderLoader.NAMESPACE.equals(reader.getNamespaceURI())) { + return true; + } + if (!"routes".equals(reader.getLocalName())) { + return false; + } + } else if (depth == 2 && "semantic".equals(reader.getLocalName())) { + return true; + } + } else if (event == XMLStreamConstants.END_ELEMENT) { + depth--; + } + } + return false; + } finally { + reader.close(); + } + } + } +} diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java index a4c6b0774d508..7be5c330fa001 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java @@ -41,10 +41,11 @@ import org.apache.camel.support.RoutesBuilderLoaderSupport; import org.apache.camel.xml.in.ModelParser; -/** Loads standalone declarations or declarations alongside routes from {@code *.semantic.xml} resources. */ +/** Parses standalone semantic declarations or declarations alongside XML routes. */ @RoutesLoader("semantic.xml") public class SemanticXmlRoutesBuilderLoader extends RoutesBuilderLoaderSupport { - private static final Set NAMESPACES = Set.of("", "http://camel.apache.org/schema/semantic", + static final String NAMESPACE = "http://camel.apache.org/schema/semantic"; + private static final Set NAMESPACES = Set.of("", NAMESPACE, "http://camel.apache.org/schema/xml-io", "http://camel.apache.org/schema/spring"); @Override @@ -188,7 +189,9 @@ private static List children(Element parent) { List answer = new ArrayList<>(); for (Node child = parent.getFirstChild(); child != null; child = child.getNextSibling()) { if (child instanceof Element element) { - if (!namespace(parent).equals(namespace(element))) { + boolean semanticBlock = "routes".equals(parent.getLocalName()) + && "semantic".equals(element.getLocalName()) && NAMESPACE.equals(namespace(element)); + if (!semanticBlock && !namespace(parent).equals(namespace(element))) { throw new IllegalArgumentException("Unexpected namespace on " + element.getTagName()); } answer.add(element); diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java index 8a396b98c76fe..9d56c50ff1d2f 100644 --- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java +++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticDeclarationDslTest.java @@ -114,7 +114,8 @@ private static String document(String root, String contents) { } @ParameterizedTest - @ValueSource(strings = { "java", "routes", "standalone", "no-namespace", "component-namespace", "xml-io-namespace" }) + @ValueSource(strings = { + "java", "routes", "standalone", "no-namespace", "component-namespace", "xml-io-namespace", "semantic-extension" }) void componentDeclarationsEvaluateSingleQuestionsAndMixedBatches(String dsl) throws Exception { if (dsl.equals("java")) { context.addRoutes(new RouteBuilder() { @@ -139,7 +140,7 @@ public void configure() { if (dsl.equals("standalone")) { PluginHelper.getRoutesLoader(context).loadRoutes(List.of( ResourceHelper.fromString("use.xml", document("routes", route)), - ResourceHelper.fromString("questions.semantic.xml", xmlQuestions()))); + ResourceHelper.fromString("questions.xml", xmlQuestions()))); } else { String xml = document("routes", route + xmlQuestions()); if (dsl.equals("no-namespace")) { @@ -149,7 +150,7 @@ public void configure() { } else if (dsl.equals("xml-io-namespace")) { xml = xml.replace("schema/spring", "schema/xml-io"); } - load("questions.semantic.xml", xml); + load(dsl.equals("semantic-extension") ? "questions.semantic.xml" : "questions.xml", xml); } } assertThat(states).isEmpty(); @@ -180,15 +181,15 @@ public void configure() { @ParameterizedTest @MethodSource("invalidXml") void invalidDeclarationsLeaveThePreviousDefinitionsIntact(String declaration, String message) throws Exception { - load("questions.semantic.xml", document("routes", xmlQuestions())); + load("questions.xml", document("routes", xmlQuestions())); SemanticQuestion previous = SemanticQuestions.get(context).get("department"); assertThatThrownBy(() -> PluginHelper.getRoutesLoader(context).updateRoutes( - ResourceHelper.fromString("questions.semantic.xml", + ResourceHelper.fromString("questions.xml", document("routes", "" + declaration + "")))) .hasMessageContaining(message); assertThat(SemanticQuestions.get(context).get("department")).isSameAs(previous); assertThat(states).isEmpty(); - PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.semantic.xml", + PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.xml", document("routes", xmlQuestions().replace("header.myState", "header.corrected")))); assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${header.corrected}"); } @@ -289,7 +290,7 @@ public void configure() { questions(semanticQuestions(this)); } }); - assertThatThrownBy(() -> load("duplicate.semantic.xml", document("routes", xmlQuestions()))) + assertThatThrownBy(() -> load("duplicate.xml", document("routes", xmlQuestions()))) .hasMessageContaining("Duplicate semantic question"); assertThat(states).isEmpty(); } @@ -306,13 +307,13 @@ public void configure() { } }); } else { - load("questions.semantic.xml", document("routes", xmlQuestions() + load("questions.xml", document("routes", xmlQuestions() .replace("threshold=\"0.8\"", "threshold=\"{{threshold:0.8}}\"") .replace("uncertainty=\"0.1\"", "uncertainty=\"{{uncertainty:0.1}}\""))); } assertThat(SemanticQuestions.get(context).get("urgent").getThreshold()).isEqualTo(0.8); assertThat(SemanticQuestions.get(context).get("urgent").getUncertainty()).isEqualTo(0.1); - assertThatThrownBy(() -> load("invalid.semantic.xml", document("routes", """ + assertThatThrownBy(() -> load("invalid.xml", document("routes", """ Urgent? @@ -327,13 +328,13 @@ void failedResourceBatchDoesNotReuseEarlierCachedDeclarationsOnRetry() throws Ex + "Valid?"; var loader = PluginHelper.getRoutesLoader(context); assertThatThrownBy(() -> loader.loadRoutes(List.of( - ResourceHelper.fromString("first.semantic.xml", document("routes", xmlQuestions())), - ResourceHelper.fromString("second.semantic.xml", document("routes", second.replace("boolean", "unknown")))))) + ResourceHelper.fromString("first.xml", document("routes", xmlQuestions())), + ResourceHelper.fromString("second.xml", document("routes", second.replace("boolean", "unknown")))))) .hasMessageContaining("Invalid semantic question"); loader.loadRoutes(List.of( - ResourceHelper.fromString("first.semantic.xml", + ResourceHelper.fromString("first.xml", document("routes", xmlQuestions().replace("header.myState", "header.new"))), - ResourceHelper.fromString("second.semantic.xml", document("routes", second)))); + ResourceHelper.fromString("second.xml", document("routes", second)))); assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${header.new}"); assertThat(SemanticQuestions.get(context).get("second")).isNotNull(); assertThat(states).isEmpty(); @@ -347,7 +348,7 @@ void declarationsFromAnotherResourceAreInstalledBeforeReferences() throws Except ref:department """)), - ResourceHelper.fromString("definitions.semantic.xml", document("routes", xmlQuestions())))); + ResourceHelper.fromString("definitions.xml", document("routes", xmlQuestions())))); assertThat(states).isEmpty(); try (var template = context.createProducerTemplate()) { assertThat(template.requestBodyAndHeader("direct:use", "original", "myState", "invoice")).isEqualTo("billing"); @@ -366,30 +367,30 @@ void unknownReferenceFailsAtRouteInitialization() { @Test void reloadingReplacesAndRemovesDeclarationsUsedByExistingExpressions() throws Exception { - load("questions.semantic.xml", document("routes", xmlQuestions())); + load("questions.xml", document("routes", xmlQuestions())); var expression = context.resolveLanguage("semantic").createExpression("ref:department"); var exchange = new DefaultExchange(context); exchange.getMessage().setHeader("myState", "old"); assertThat(expression.evaluate(exchange, String.class)).isEqualTo("billing"); - PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.semantic.xml", + PluginHelper.getRoutesLoader(context).updateRoutes(ResourceHelper.fromString("questions.xml", document("routes", xmlQuestions().replace("header.myState", "header.updated")))); exchange.getMessage().setHeader("updated", "new"); assertThat(expression.evaluate(exchange, String.class)).isEqualTo("billing"); assertThat(states).containsExactly("old", "new"); PluginHelper.getRoutesLoader(context) - .updateRoutes(ResourceHelper.fromString("questions.semantic.xml", document("routes", ""))); + .updateRoutes(ResourceHelper.fromString("questions.xml", document("routes", ""))); assertThatThrownBy(() -> expression.evaluate(exchange, Object.class)).hasMessageContaining("Unknown semantic question"); } @Test void watcherRemovesDeletedAndRenamedDeclarationResources() throws Exception { - Path original = directory.resolve("questions.semantic.xml"); + Path original = directory.resolve("questions.xml"); Files.writeString(original, document("routes", xmlQuestions())); Resource source = ResourceHelper.resolveResource(context, original.toUri().toString()); PluginHelper.getRoutesLoader(context).loadRoutes(source); TestWatcher watcher = new TestWatcher(); watcher.setCamelContext(context); - Path renamed = Files.move(original, directory.resolve("renamed.semantic.xml")); + Path renamed = Files.move(original, directory.resolve("renamed.xml")); watcher.reload(source); assertThatThrownBy(() -> SemanticQuestions.get(context).get("department")).hasMessageContaining("Unknown"); Resource replacement = ResourceHelper.resolveResource(context, renamed.toUri().toString()); @@ -404,9 +405,9 @@ void watcherRemovesDeletedAndRenamedDeclarationResources() throws Exception { @ParameterizedTest @ValueSource(strings = { "", "http://camel.apache.org/schema/semantic", "http://camel.apache.org/schema/xml-io" }) void standaloneXmlSupportsOptionalNamespace(String namespace) throws Exception { - load("questions.semantic.xml", xmlQuestions().replace("", "")); + load("questions.xml", xmlQuestions().replace("", "")); assertThat(SemanticQuestions.get(context).get("department").getCriteria()).containsKey("billing"); - load("questions.semantic.xml", ""); + load("questions.xml", ""); assertThat(SemanticQuestions.get(context).isEmpty()).isTrue(); } @@ -442,16 +443,16 @@ public void configure() { "Hi" }) void malformedXmlDoesNotReplacePreviousDefinitions(String xml) throws Exception { - load("questions.semantic.xml", xmlQuestions()); + load("questions.xml", xmlQuestions()); var previous = SemanticQuestions.get(context).get("department"); - assertThatThrownBy(() -> load("questions.semantic.xml", xml)).isInstanceOf(Exception.class); + assertThatThrownBy(() -> load("questions.xml", xml)).isInstanceOf(Exception.class); assertThat(SemanticQuestions.get(context).get("department")).isSameAs(previous); } @ParameterizedTest @ValueSource(strings = { "java", "xml" }) void loadingPlainBuilderDiscardsDeletedDeclarationResources(String dsl) throws Exception { - Path file = directory.resolve(dsl.equals("java") ? "Questions.java" : "questions.semantic.xml"); + Path file = directory.resolve(dsl.equals("java") ? "Questions.java" : "questions.xml"); Files.writeString(file, xmlQuestions()); Resource resource = ResourceHelper.resolveResource(context, file.toUri().toString()); if (dsl.equals("java")) { @@ -476,6 +477,22 @@ public void configure() { assertThat(SemanticQuestions.get(context).isEmpty()).isTrue(); } + @Test + void semanticNamespaceCanBeUsedInsideStandardXmlRoutes() throws Exception { + load("questions.xml", document("routes", xmlQuestions().replace("", + ""))); + assertThat(SemanticQuestions.get(context).get("department").getCriteria()).containsKey("billing"); + } + + @Test + void failedOrdinaryReplacementKeepsPreviousDeclarations() throws Exception { + load("questions.xml", xmlQuestions()); + var previous = SemanticQuestions.get(context).get("department"); + assertThatThrownBy(() -> load("questions.xml", "")) + .isInstanceOf(Exception.class); + assertThat(SemanticQuestions.get(context).get("department")).isSameAs(previous); + } + private void load(String location, String xml) throws Exception { PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString(location, xml)); } diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java new file mode 100644 index 0000000000000..ee784482f1fb2 --- /dev/null +++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java @@ -0,0 +1,181 @@ +/* + * 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.semantic; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.camel.dsl.xml.io.XmlRoutesBuilderLoader; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.impl.engine.DefaultClassResolver; +import org.apache.camel.impl.engine.DefaultRoutesLoader; +import org.apache.camel.main.Main; +import org.apache.camel.spi.RoutesBuilderLoader; +import org.apache.camel.spi.RoutesLoader; +import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.ResourceHelper; +import org.apache.camel.support.SimpleRegistry; +import org.apache.camel.support.service.ServiceSupport; +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 static org.assertj.core.api.Assertions.assertThat; + +class SemanticXmlAutoDiscoveryTest { + @TempDir + Path directory; + + @ParameterizedTest + @CsvSource({ "false,routes.xml", "true,routes.xml", "false,routes.camel.xml", "true,routes.camel.xml" }) + void mainLoadsOrdinaryXmlWithoutLoaderRegistration(boolean standalone, String filename) throws Exception { + String declarations = """ + + + Which department? + + + + """; + String route = """ + + ref:department + + """; + Path routes = directory.resolve(filename); + String files; + if (standalone) { + Path questions = directory.resolve("questions.xml"); + Files.writeString(questions, declarations); + Files.writeString(routes, "" + route + ""); + // The consumer is deliberately listed first. + files = routes.toUri() + "," + questions.toUri(); + } else { + Files.writeString(routes, "" + declarations + route + ""); + files = routes.toUri().toString(); + } + Main main = new Main(); + main.bind("classifier", new SemanticLanguageTest.LabelAdapter()); + main.addProperty("camel.language.semantic.adapter", "classifier"); + main.configure().setRoutesIncludePattern(files); + try { + main.start(); + try (var template = main.getCamelContext().createProducerTemplate()) { + assertThat(template.requestBody("direct:tickets", "invoice", String.class)).isEqualTo("billing"); + } + } finally { + main.stop(); + } + } + + @Test + void ordinaryXmlRetainsBeansRouteConfigurationsAndDelegateLifecycle() throws Exception { + AtomicReference delegate = new AtomicReference<>(); + try (var context = new DefaultCamelContext()) { + context.getCamelContextExtension().addContextPlugin(RoutesLoader.class, new DefaultRoutesLoader(context) { + @Override + public void initRoutesBuilderLoader(RoutesBuilderLoader loader) { + delegate.set(loader); + } + }); + context.start(); + PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("ordinary.xml", """ + + + + java.lang.IllegalArgumentException + true + handled + + + + + + + + """)); + assertThat(context.getRouteConfigurationDefinitions()).hasSize(1); + assertThat(context.getRegistry().lookupByName("counter")).isInstanceOf(AtomicInteger.class); + assertThat(context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)).isNull(); + try (var template = context.createProducerTemplate()) { + assertThat(template.requestBody("direct:ordinary", "test", Integer.class)).isEqualTo(1); + } + assertThat(delegate.get()).isInstanceOf(XmlRoutesBuilderLoader.class); + assertThat(((ServiceSupport) delegate.get()).isStarted()).isTrue(); + } + assertThat(((ServiceSupport) delegate.get()).isStopped()).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = { "routes-builder-loader-xml", "customXmlLoader" }) + void applicationRegisteredXmlLoaderTakesPrecedence(String name) throws Exception { + XmlRoutesBuilderLoader custom = new XmlRoutesBuilderLoader(); + try (var context = new DefaultCamelContext()) { + context.getRegistry().bind(name, custom); + context.build(); + assertThat(PluginHelper.getRoutesLoader(context).getRoutesLoader("xml")).isSameAs(custom); + assertThat(context.getRegistry().findByType(SemanticXmlLoader.class)) + .allMatch(loader -> !loader.isSupportedExtension("xml")); + } + } + + @Test + void contextWithoutOptionalXmlSupportDoesNotInstallWrapper() throws Exception { + try (var context = new DefaultCamelContext(false)) { + context.setClassResolver(new DefaultClassResolver(context) { + @Override + public Class resolveClass(String name) { + return "org.apache.camel.xml.in.ModelParser".equals(name) ? null : super.resolveClass(name); + } + }); + context.build(); + assertThat(context.getRegistry().lookupByName(SemanticXmlLoader.REGISTRY_KEY)).isNull(); + assertThat(context.resolveLanguage("semantic")).isNotNull(); + } + } + + @Test + void contextWithApplicationRegistryDiscoversXmlDeclarations() throws Exception { + try (var context = new DefaultCamelContext(new SimpleRegistry())) { + context.start(); + PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("questions.xml", """ + Urgent? + """)); + assertThat(SemanticQuestions.get(context).get("urgent").getInstructions()).isEqualTo("Urgent?"); + } + } + + @Test + void contextRestartReinstallsAutomaticLoader() throws Exception { + try (var context = new DefaultCamelContext()) { + for (int i = 0; i < 2; i++) { + context.start(); + PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("questions.xml", + """ + Urgent? + """)); + assertThat(SemanticQuestions.get(context).get("urgent").getInstructions()).isEqualTo("Urgent?"); + context.stop(); + assertThat(context.getRegistry().lookupByName(SemanticXmlLoader.REGISTRY_KEY)).isNull(); + } + } + } +} diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc index 04d6027f2deda..4dcd84d5341ca 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc @@ -3780,6 +3780,13 @@ JAXP external-access model and remain resolvable. Stylesheet `xsl:include` / `xs are resolved at compile time (they are part of the route definition, authored by the route author) and are unaffected by this change. +=== camel-semantic - automatic XML route loading + +When `camel-semantic` and the XML DSL are on the classpath, Camel now automatically detects +semantic declarations in ordinary XML route resources. XML documents without declarations +continue through the standard XML loader. Explicitly registered XML loaders retain precedence. +This changes route resource discovery only; Spring XML application-context parsing is unaffected. + == ThrottlingExceptionRoutePolicy `ThrottlingExceptionRoutePolicy.setKeepOpen(true)` now opens the circuit immediately and synchronously (the consumer From dc1a33c6385a9071bda110e492d21bf130e68abe Mon Sep 17 00:00:00 2001 From: Luigi De Masi Date: Wed, 30 Sep 2026 11:14:33 +0200 Subject: [PATCH 09/11] CAMEL-25138: Preserve XML source positions and decouple MCP guard Parse routes from their original resource bytes while skipping the validated semantic declaration subtree. Accept dotted XML filenames through the component loader and retain custom loader precedence. Discover the optional semantic registry at runtime before generic MCP exports, preserving rejection of lossy conversions without a production dependency on camel-semantic. Cover original locations and diagnostics, dotted filenames, and empty registries; document the XML filename support. Co-authored-by: Codex Signed-off-by: Luigi De Masi --- .../camel/catalog/docs/semantic-language.adoc | 2 + .../src/main/docs/semantic-language.adoc | 2 + .../camel/semantic/SemanticXmlLoader.java | 2 +- .../SemanticXmlRoutesBuilderLoader.java | 44 +++++++---- .../SemanticXmlAutoDiscoveryTest.java | 76 ++++++++++++++++++- dsl/camel-jbang/camel-jbang-mcp/pom.xml | 10 +-- .../core/commands/mcp/TransformTools.java | 12 ++- .../core/commands/mcp/TransformToolsTest.java | 19 +++++ 8 files changed, 141 insertions(+), 26 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 4f37730d403bf..41c3797b928ba 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 @@ -148,6 +148,8 @@ automatically at startup: use ordinary `*.xml` files without registering a loade the Camel core model. XML documents without semantic declarations are handled by the standard XML loader, including its bean and route configuration support. XML loaders registered by the application before route loading retain precedence over automatic discovery. +Filenames may contain dots, such as `my.tickets.xml`. Routes retain their original resource +locations and line numbers for debugging and error messages. Both layouts are supported. To keep declarations alongside routes, use `tickets.xml`: 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 4f37730d403bf..41c3797b928ba 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 @@ -148,6 +148,8 @@ automatically at startup: use ordinary `*.xml` files without registering a loade the Camel core model. XML documents without semantic declarations are handled by the standard XML loader, including its bean and route configuration support. XML loaders registered by the application before route loading retain precedence over automatic discovery. +Filenames may contain dots, such as `my.tickets.xml`. Routes retain their original resource +locations and line numbers for debugging and error messages. Both layouts are supported. To keep declarations alongside routes, use `tickets.xml`: diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java index dd2d06e2e7236..e5d829e9e62bf 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java @@ -47,7 +47,7 @@ public String getSupportedExtension() { @Override public boolean isSupportedExtension(String extension) { // DefaultCamelContext may build before application beans are bound. Give later custom loaders precedence too. - return ("xml".equals(extension) || "camel.xml".equals(extension)) + return ("xml".equals(extension) || extension.endsWith(".xml")) && getCamelContext().getRegistry().findByType(RoutesBuilderLoader.class).stream() .noneMatch(loader -> loader != this && loader.isSupportedExtension(extension)); } diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java index 7be5c330fa001..fcbc0e9986b7b 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlRoutesBuilderLoader.java @@ -16,18 +16,14 @@ */ package org.apache.camel.semantic; +import java.io.IOException; import java.io.InputStream; -import java.io.StringReader; -import java.io.StringWriter; import java.util.ArrayList; import java.util.List; import java.util.Set; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -40,6 +36,7 @@ import org.apache.camel.spi.annotations.RoutesLoader; import org.apache.camel.support.RoutesBuilderLoaderSupport; import org.apache.camel.xml.in.ModelParser; +import org.apache.camel.xml.io.XmlPullParserException; /** Parses standalone semantic declarations or declarations alongside XML routes. */ @RoutesLoader("semantic.xml") @@ -104,19 +101,14 @@ private RoutesDefinition parse(Resource resource) throws Exception { } found = true; declarations(child, questions); - root.removeChild(child); } else if (!"route".equals(child.getLocalName())) { throw new IllegalArgumentException("Unexpected element in routes: " + child.getTagName()); } } - TransformerFactory transformer = TransformerFactory.newInstance(); - transformer.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); - transformer.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); - transformer.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); - StringWriter xml = new StringWriter(); - transformer.newTransformer().transform(new DOMSource(root), new StreamResult(xml)); - routes = new ModelParser(new StringReader(xml.toString()), namespace).parseRoutesDefinition() - .orElseThrow(() -> new IllegalArgumentException("Expected XML routes")); + try (InputStream stream = resource.getInputStream()) { + routes = new SemanticModelParser(resource, stream, namespace).parseRoutesDefinition() + .orElseThrow(() -> new IllegalArgumentException("Expected XML routes")); + } } else { throw new IllegalArgumentException("Expected semantic or routes root element"); } @@ -125,6 +117,30 @@ private RoutesDefinition parse(Resource resource) throws Exception { return routes; } + private static final class SemanticModelParser extends ModelParser { + private SemanticModelParser( + Resource resource, InputStream stream, String namespace) + throws IOException, + XmlPullParserException { + super(stream, namespace); + this.resource = resource; + } + + @Override + protected boolean handleUnexpectedElement(String namespace, String name) throws XmlPullParserException { + if ("semantic".equals(name) && parser.getDepth() == 2) { + // The DOM pass already validated this block. Keep the original route bytes and source positions. + try { + parser.skipSubTree(); + } catch (IOException e) { + throw new XmlPullParserException("Cannot read semantic declaration", parser, e); + } + return true; + } + return super.handleUnexpectedElement(namespace, name); + } + } + private static void declarations(Element semantic, SemanticQuestionsBuilder questions) { attributes(semantic, Set.of()); for (Element element : children(semantic)) { diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java index ee784482f1fb2..8a3a3f5fcb5c9 100644 --- a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java +++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlAutoDiscoveryTest.java @@ -32,6 +32,7 @@ import org.apache.camel.support.ResourceHelper; import org.apache.camel.support.SimpleRegistry; import org.apache.camel.support.service.ServiceSupport; +import org.apache.camel.xml.io.XmlPullParserLocationException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; @@ -39,13 +40,34 @@ import org.junit.jupiter.params.provider.ValueSource; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class SemanticXmlAutoDiscoveryTest { + private static final String LOCATED_ROUTES = """ + + + + + Urgent? + + + + + + ${body} != null + + + + + """; + @TempDir Path directory; @ParameterizedTest - @CsvSource({ "false,routes.xml", "true,routes.xml", "false,routes.camel.xml", "true,routes.camel.xml" }) + @CsvSource({ + "false,routes.xml", "true,routes.xml", "false,routes.camel.xml", "true,routes.camel.xml", + "false,my.tickets.xml", "true,my.tickets.xml", "false,my.tickets.semantic.xml", "true,my.tickets.semantic.xml" }) void mainLoadsOrdinaryXmlWithoutLoaderRegistration(boolean standalone, String filename) throws Exception { String declarations = """ @@ -63,7 +85,7 @@ void mainLoadsOrdinaryXmlWithoutLoaderRegistration(boolean standalone, String fi Path routes = directory.resolve(filename); String files; if (standalone) { - Path questions = directory.resolve("questions.xml"); + Path questions = directory.resolve("my.questions.xml"); Files.writeString(questions, declarations); Files.writeString(routes, "" + route + ""); // The consumer is deliberately listed first. @@ -86,6 +108,56 @@ void mainLoadsOrdinaryXmlWithoutLoaderRegistration(boolean standalone, String fi } } + @ParameterizedTest + @ValueSource(strings = { "tickets.xml", "tickets.semantic.xml", "my.tickets.xml", "my.tickets.semantic.xml" }) + void declarationsPreserveOriginalRouteSourceLocations(String filename) throws Exception { + try (var context = new DefaultCamelContext()) { + context.setSourceLocationEnabled(true); + PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString(filename, LOCATED_ROUTES)); + var route = context.getRouteDefinitions().get(0); + assertThat(route.getLocation()).isEqualTo(filename); + assertThat(route.getLineNumber()).isEqualTo(8); + assertThat(route.getInput().getLocation()).isEqualTo(filename); + assertThat(route.getInput().getLineNumber()).isEqualTo(9); + var filter = route.getOutputs().get(0); + assertThat(filter.getLocation()).isEqualTo(filename); + assertThat(filter.getLineNumber()).isEqualTo(10); + assertThat(filter.getOutputs().get(0).getLocation()).isEqualTo(filename); + assertThat(filter.getOutputs().get(0).getLineNumber()).isEqualTo(12); + } + } + + @Test + void nestedSemanticElementIsRejectedWithOriginalSourceLocation() throws Exception { + try (var context = new DefaultCamelContext()) { + context.setSourceLocationEnabled(true); + String xml = LOCATED_ROUTES.replace("", ""); + assertThatThrownBy(() -> PluginHelper.getRoutesLoader(context) + .loadRoutes(ResourceHelper.fromString("invalid.tickets.xml", xml))) + .isInstanceOfSatisfying(XmlPullParserLocationException.class, error -> { + assertThat(error.getResource().getLocation()).isEqualTo("invalid.tickets.xml"); + assertThat(error.getLineNumber()).isEqualTo(12); + assertThat(error).hasMessageContaining("invalid.tickets.xml, line 12") + .hasMessageContaining(""); + }); + assertThat(SemanticQuestions.get(context).isEmpty()).isTrue(); + } + } + + @Test + void applicationLoaderForDottedExtensionTakesPrecedence() throws Exception { + XmlRoutesBuilderLoader custom = new XmlRoutesBuilderLoader() { + @Override + public boolean isSupportedExtension(String extension) { + return "tickets.xml".equals(extension); + } + }; + try (var context = new DefaultCamelContext()) { + context.getRegistry().bind("ticketsLoader", custom); + assertThat(PluginHelper.getRoutesLoader(context).getRoutesLoader("tickets.xml")).isSameAs(custom); + } + } + @Test void ordinaryXmlRetainsBeansRouteConfigurationsAndDelegateLifecycle() throws Exception { AtomicReference delegate = new AtomicReference<>(); diff --git a/dsl/camel-jbang/camel-jbang-mcp/pom.xml b/dsl/camel-jbang/camel-jbang-mcp/pom.xml index 227616abc2bbb..40786dbc0ab90 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/pom.xml +++ b/dsl/camel-jbang/camel-jbang-mcp/pom.xml @@ -116,11 +116,6 @@ camel-java-joor-dsl - - org.apache.camel - camel-semantic - - io.swagger.core.v3 @@ -152,6 +147,11 @@ + + org.apache.camel + camel-semantic + test + org.apache.camel camel-xml-io-dsl diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java index 8ab79c7e752c9..d068f24032454 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java @@ -34,7 +34,6 @@ import org.apache.camel.model.ProcessorDefinitionHelper; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.RoutesDefinition; -import org.apache.camel.semantic.SemanticQuestions; import org.apache.camel.spi.Resource; import org.apache.camel.support.PluginHelper; import org.apache.camel.support.ResourceHelper; @@ -170,9 +169,14 @@ private String transformYamlToXml(String yaml) throws Exception { } } - private static void requireSeparateDeclarations(DefaultCamelContext context) { - SemanticQuestions questions = context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class); - if (questions != null && !questions.isEmpty()) { + private static void requireSeparateDeclarations(DefaultCamelContext context) throws ReflectiveOperationException { + // Semantic declarations are optional and live outside the model exported by this converter. + Class type = context.getClassResolver().resolveClass("org.apache.camel.semantic.SemanticQuestions"); + if (type == null) { + return; + } + Object questions = context.getCamelContextExtension().getContextPlugin(type); + if (questions != null && !(boolean) type.getMethod("isEmpty").invoke(questions)) { throw new IllegalArgumentException( "Semantic declarations cannot be exported by the generic route converter. " + "Keep them in a separate declaration resource and convert only the routes."); diff --git a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java index ee2b90c181be5..a5dc8ed53fb3b 100644 --- a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java +++ b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java @@ -69,6 +69,25 @@ void genericConversionDoesNotSilentlyLoseSemanticDeclarations(String source, Str .isInstanceOf(ToolCallException.class).hasMessageContaining("Keep them in a separate declaration resource"); } + @ParameterizedTest + @ValueSource(strings = { "xml", "yaml" }) + void emptySemanticRegistryDoesNotPreventConversion(String target) { + String route = """ + import org.apache.camel.builder.RouteBuilder; + import static org.apache.camel.semantic.SemanticQuestionsBuilder.semanticQuestions; + public class EmptySemanticRoute extends RouteBuilder { + public void configure() { + semanticQuestions(this).register(); + from("direct:input").log("Hello"); + } + } + """; + var result = createTools().camel_transform_route(route, "java", target); + + assertThat(result.supported).isTrue(); + assertThat(result.result).contains("direct:input").doesNotContain("semantic"); + } + @ParameterizedTest @CsvSource({ "yaml,xml", "java,xml", "java,yaml" }) void semanticConversionReportsMissingNumericProperty(String source, String target) { From a52e1b07848193c97a5878e7794fe67ce240ed88 Mon Sep 17 00:00:00 2001 From: Luigi De Masi Date: Wed, 30 Sep 2026 13:17:37 +0200 Subject: [PATCH 10/11] CAMEL-25138: Streamline semantic XML discovery Limit declaration detection to top-level elements, skip route subtrees and stop at the closing routes tag while allowing declarations after routes. Reuse lazily discovered application loaders and refresh discovery at startup, reload and registry replacement instead of scanning on each check. Remove the migration-guide note for the new component and retain its component documentation. Cover discovery reuse, refresh and late XML declarations with regression tests. Co-authored-by: Codex Signed-off-by: Luigi De Masi --- .../camel/catalog/docs/semantic-language.adoc | 3 +- .../src/main/docs/semantic-language.adoc | 3 +- .../camel/semantic/SemanticReloadPlugin.java | 8 +- .../camel/semantic/SemanticXmlLoader.java | 51 ++++++- .../camel/semantic/SemanticXmlLoaderTest.java | 124 ++++++++++++++++++ .../pages/camel-4x-upgrade-guide-4_23.adoc | 7 - 6 files changed, 180 insertions(+), 16 deletions(-) create mode 100644 components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java 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 41c3797b928ba..c654e13feb351 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 @@ -147,7 +147,8 @@ xref:others:java-xml-io-dsl.adoc[XML DSL] (`camel-xml-io-dsl`). Camel discovers automatically at startup: use ordinary `*.xml` files without registering a loader or changing the Camel core model. XML documents without semantic declarations are handled by the standard XML loader, including its bean and route configuration support. XML loaders registered by the -application before route loading retain precedence over automatic discovery. +application before route resource discovery retain precedence over automatic discovery. +Loader discovery is refreshed when the context starts or routes are reloaded. Filenames may contain dots, such as `my.tickets.xml`. Routes retain their original resource locations and line numbers for debugging and error messages. 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 41c3797b928ba..c654e13feb351 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 @@ -147,7 +147,8 @@ xref:others:java-xml-io-dsl.adoc[XML DSL] (`camel-xml-io-dsl`). Camel discovers automatically at startup: use ordinary `*.xml` files without registering a loader or changing the Camel core model. XML documents without semantic declarations are handled by the standard XML loader, including its bean and route configuration support. XML loaders registered by the -application before route loading retain precedence over automatic discovery. +application before route resource discovery retain precedence over automatic discovery. +Loader discovery is refreshed when the context starts or routes are reloaded. Filenames may contain dots, such as `my.tickets.xml`. Routes retain their original resource locations and line numbers for debugging and error messages. diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java index 1fb38ad3e5020..1b43741547126 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java @@ -26,7 +26,7 @@ /** Installs optional XML declaration support and removes definitions from deleted files before route reload. */ public class SemanticReloadPlugin implements ContextServicePlugin { - private RoutesBuilderLoader xmlLoader; + private SemanticXmlLoader xmlLoader; private LifecycleStrategy lifecycle; @Override @@ -37,6 +37,9 @@ public void load(CamelContext context) { public void onContextInitializing(CamelContext camelContext) { // Applications can replace the registry after the context's eager build phase. installXmlLoader(camelContext); + if (xmlLoader != null) { + xmlLoader.resetLoaderDiscovery(); + } } }; context.addLifecycleStrategy(lifecycle); @@ -82,6 +85,9 @@ public void unload(CamelContext context) { @Override public void onReload(CamelContext context) { + if (xmlLoader != null) { + xmlLoader.resetLoaderDiscovery(); + } SemanticQuestions questions = context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class); if (questions != null) { questions.removeDeletedResources(); diff --git a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java index e5d829e9e62bf..b3be7899bd4a8 100644 --- a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java +++ b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticXmlLoader.java @@ -17,14 +17,17 @@ package org.apache.camel.semantic; import java.io.InputStream; +import java.util.List; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import org.apache.camel.RoutesBuilder; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.builder.RouteBuilderLifecycleStrategy; +import org.apache.camel.spi.Registry; import org.apache.camel.spi.Resource; import org.apache.camel.spi.RoutesBuilderLoader; import org.apache.camel.support.CachedResource; @@ -38,6 +41,8 @@ final class SemanticXmlLoader extends RoutesBuilderLoaderSupport { static final String REGISTRY_KEY = "semantic-xml-routes-loader"; private RoutesBuilderLoader delegate; private final SemanticXmlRoutesBuilderLoader semantic = new SemanticXmlRoutesBuilderLoader(); + private Registry loaderRegistry; + private List otherLoaders = List.of(); @Override public String getSupportedExtension() { @@ -46,10 +51,23 @@ public String getSupportedExtension() { @Override public boolean isSupportedExtension(String extension) { - // DefaultCamelContext may build before application beans are bound. Give later custom loaders precedence too. return ("xml".equals(extension) || extension.endsWith(".xml")) - && getCamelContext().getRegistry().findByType(RoutesBuilderLoader.class).stream() - .noneMatch(loader -> loader != this && loader.isSupportedExtension(extension)); + && otherLoaders().stream().noneMatch(loader -> loader.isSupportedExtension(extension)); + } + + private synchronized List otherLoaders() { + Registry registry = getCamelContext().getRegistry(); + if (loaderRegistry != registry) { + // Discover lazily: the context may build before application loaders are registered. + otherLoaders = registry.findByType(RoutesBuilderLoader.class).stream().filter(loader -> loader != this).toList(); + loaderRegistry = registry; + } + return otherLoaders; + } + + synchronized void resetLoaderDiscovery() { + loaderRegistry = null; + otherLoaders = List.of(); } @Override @@ -107,6 +125,7 @@ private synchronized RoutesBuilderLoader delegate() throws Exception { protected void doStop() throws Exception { ServiceHelper.stopAndShutdownServices(delegate, semantic); delegate = null; + resetLoaderDiscovery(); super.doStop(); } @@ -141,11 +160,19 @@ private static boolean hasDeclarations(Resource resource) throws Exception { if (!"routes".equals(reader.getLocalName())) { return false; } - } else if (depth == 2 && "semantic".equals(reader.getLocalName())) { - return true; + } else if (depth == 2) { + if ("semantic".equals(reader.getLocalName())) { + return true; + } + // Only direct children can declare questions. Consume nested route content without + // inspecting names or namespaces, then continue looking for declarations after routes. + skipSubtree(reader); + depth--; } } else if (event == XMLStreamConstants.END_ELEMENT) { - depth--; + if (--depth == 0) { + return false; + } } } return false; @@ -154,4 +181,16 @@ private static boolean hasDeclarations(Resource resource) throws Exception { } } } + + private static void skipSubtree(XMLStreamReader reader) throws XMLStreamException { + int depth = 1; + while (depth != 0) { + int event = reader.next(); + if (event == XMLStreamConstants.START_ELEMENT) { + depth++; + } else if (event == XMLStreamConstants.END_ELEMENT) { + depth--; + } + } + } } diff --git a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java new file mode 100644 index 0000000000000..2829377894d75 --- /dev/null +++ b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticXmlLoaderTest.java @@ -0,0 +1,124 @@ +/* + * 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.semantic; + +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.camel.dsl.xml.io.XmlRoutesBuilderLoader; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.spi.ContextServiceLoaderPluginResolver; +import org.apache.camel.spi.RoutesBuilderLoader; +import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.ResourceHelper; +import org.apache.camel.support.SimpleRegistry; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class SemanticXmlLoaderTest { + @Test + void loaderDiscoveryIsReusedAndRefreshedOnReload() throws Exception { + AtomicInteger lookups = new AtomicInteger(); + SimpleRegistry registry = new SimpleRegistry() { + @Override + public Set findByType(Class type) { + if (type == RoutesBuilderLoader.class) { + lookups.incrementAndGet(); + } + return super.findByType(type); + } + }; + try (var context = new DefaultCamelContext(registry)) { + context.start(); + SemanticXmlLoader loader = registry.lookupByNameAndType(SemanticXmlLoader.REGISTRY_KEY, SemanticXmlLoader.class); + lookups.set(0); + for (String extension : new String[] { "xml", "tickets.xml", "semantic.xml", "xml" }) { + assertThat(loader.isSupportedExtension(extension)).isTrue(); + } + assertThat(loader.isSupportedExtension("yaml")).isFalse(); + assertThat(lookups).hasValue(1); + + registry.bind("customXml", new XmlRoutesBuilderLoader()); + var plugins = context.getCamelContextExtension().getContextPlugin(ContextServiceLoaderPluginResolver.class); + plugins.onReload(); + assertThat(loader.isSupportedExtension("xml")).isFalse(); + assertThat(loader.isSupportedExtension("xml")).isFalse(); + assertThat(lookups).hasValue(2); + + registry.unbind("customXml"); + plugins.onReload(); + assertThat(loader.isSupportedExtension("xml")).isTrue(); + assertThat(loader.isSupportedExtension("tickets.xml")).isTrue(); + assertThat(lookups).hasValue(3); + } + } + + @Test + void startupRefreshesDiscoveryAfterApplicationRegistersLoader() throws Exception { + try (var context = new DefaultCamelContext()) { + SemanticXmlLoader loader = context.getRegistry() + .lookupByNameAndType(SemanticXmlLoader.REGISTRY_KEY, SemanticXmlLoader.class); + assertThat(loader.isSupportedExtension("xml")).isTrue(); + XmlRoutesBuilderLoader custom = new XmlRoutesBuilderLoader(); + context.getRegistry().bind("customXml", custom); + + context.start(); + + assertThat(loader.isSupportedExtension("xml")).isFalse(); + assertThat(PluginHelper.getRoutesLoader(context).getRoutesLoader("xml")).isSameAs(custom); + } + } + + @ParameterizedTest + @ValueSource(booleans = { false, true }) + void nestedRoutesDoNotHideFollowingDeclarations(boolean declarations) throws Exception { + String routes = """ + + + + + ${body} != null + ]]> + + + + + %s + + """.formatted(declarations ? """ + + Urgent? + + """ : ""); + try (var context = new DefaultCamelContext()) { + PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("nested.routes.xml", routes)); + context.start(); + assertThat(context.getRouteDefinitions()).hasSize(2); + if (declarations) { + assertThat(SemanticQuestions.get(context).get("urgent").getInstructions()).isEqualTo("Urgent?"); + } else { + assertThat(context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class)).isNull(); + } + try (var template = context.createProducerTemplate()) { + assertThat(template.requestBody("direct:nested", "hello", String.class)).isEqualTo(""); + } + } + } +} diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc index 4dcd84d5341ca..04d6027f2deda 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc @@ -3780,13 +3780,6 @@ JAXP external-access model and remain resolvable. Stylesheet `xsl:include` / `xs are resolved at compile time (they are part of the route definition, authored by the route author) and are unaffected by this change. -=== camel-semantic - automatic XML route loading - -When `camel-semantic` and the XML DSL are on the classpath, Camel now automatically detects -semantic declarations in ordinary XML route resources. XML documents without declarations -continue through the standard XML loader. Explicitly registered XML loaders retain precedence. -This changes route resource discovery only; Spring XML application-context parsing is unaffected. - == ThrottlingExceptionRoutePolicy `ThrottlingExceptionRoutePolicy.setKeepOpen(true)` now opens the circuit immediately and synchronously (the consumer From f57b10f9c9812931cadbd53c0001636d4e050bc9 Mon Sep 17 00:00:00 2001 From: Luigi De Masi Date: Wed, 30 Sep 2026 13:52:00 +0200 Subject: [PATCH 11/11] ci: Fix downloader test proxy identity methods The service restart changes on main track services in a ConcurrentHashMap. The downloader test proxy returned null for hashCode, so all nine properties function tests failed during service registration before their assertions. Give the recording proxy identity-based hashCode and equals implementations and a useful toString without changing the downloader assertions. Failed run: https://github.com/apache/camel/actions/runs/36707716997 Co-authored-by: Codex Signed-off-by: Luigi De Masi --- ...pendencyDownloaderPropertiesFunctionResolverTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/DependencyDownloaderPropertiesFunctionResolverTest.java b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/DependencyDownloaderPropertiesFunctionResolverTest.java index 3ec98bd9517e9..8aa48cd5a981b 100644 --- a/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/DependencyDownloaderPropertiesFunctionResolverTest.java +++ b/dsl/camel-kamelet-main/src/test/java/org/apache/camel/main/download/DependencyDownloaderPropertiesFunctionResolverTest.java @@ -122,6 +122,15 @@ private static DependencyDownloader recordingDownloader(SimpleCamelContext conte new Class[] { DependencyDownloader.class }, (proxy, method, args) -> { switch (method.getName()) { + case "hashCode" -> { + return System.identityHashCode(proxy); + } + case "equals" -> { + return proxy == args[0]; + } + case "toString" -> { + return "RecordingDependencyDownloader"; + } case "downloadDependency" -> { // every overload starts with (String groupId, String artifactId, String version, ...) downloaded.add(args[0] + ":" + args[1]);