gnodet-bot commented on code in PR #27082:
URL: https://github.com/apache/camel/pull/27082#discussion_r4132812544


##########
dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java:
##########
@@ -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();

Review Comment:
   💡 **Aggressive cache clearing on error:** The catch block clears ALL caches 
(including previously-parsed valid resources) on any exception during preparse. 
This is intentional — the test 
`failedResourceBatchDoesNotReuseEarlierCachedDeclarationsOnRetry` validates 
that earlier resources in a failed batch don't retain stale cached input on 
retry. The comment explains the rationale well.
   
   However, this means a single invalid XML resource in a large batch forces 
re-parsing of ALL resources on retry, not just the failed one. For correctness 
this is the right tradeoff, but for large deployments with many XML resources, 
a per-resource rollback (tracking which resources were successfully preparsed 
and only clearing the failed one's caches + preventing semantic side-effects) 
would be more efficient. No action needed now — just flagging the tradeoff.



##########
dsl/camel-xml-io-dsl/src/main/java/org/apache/camel/dsl/xml/io/XmlRoutesBuilderLoader.java:
##########
@@ -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);
+                            });

Review Comment:
   💡 **preparse now fully parses `<routes>` documents:** Before this change, 
preparse only handled `<beans>`/`<camel>` documents; `<routes>` documents were 
parsed only in `configure()`. Now, `<routes>` documents are fully parsed during 
preparse (to extract semantic declarations), with results cached in 
`routesCache` and reused in `configure()`. This correctly avoids double-parsing 
and ensures semantic questions from co-loaded resources exist before route 
references initialize. Clean approach.



##########
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<SemanticQuestionDefinition> questions = new ArrayList<>();
+
+    public List<SemanticQuestionDefinition> getQuestions() {
+        return questions;
+    }
+
+    public void setQuestions(List<SemanticQuestionDefinition> 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;
+    }
+

Review Comment:
   💡 **Thread safety of lazy configurer discovery:** The `getContextPlugin` → 
null-check → `addContextPlugin` sequence is a TOCTOU pattern. If two threads 
both call `configure()` concurrently with a null plugin, both will discover the 
configurer and call `addContextPlugin`. In practice, route loading is 
single-threaded in Camel's lifecycle, so this is low risk — but worth a brief 
comment documenting that assumption, or switching to `computeIfAbsent` on the 
context plugin registry if that API exists.



##########
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<PropertyDefinition> criteria = new ArrayList<>();
+    @XmlElement(name = "level")

Review Comment:
   💡 **Mutable internal collections:** `getCriteria()` and `getLevels()` return 
the internal `ArrayList` directly. This is consistent with Camel's model class 
patterns (e.g., `RouteDefinition.getOutputs()`) where model objects are mutable 
builders, but callers adding elements via the fluent API (`criterion()`, 
`level()`) while another thread reads the list would cause a 
`ConcurrentModificationException`. Since model objects are built in a single 
thread during `configure()`, this follows existing Camel conventions — no 
change needed.



##########
components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java:
##########
@@ -0,0 +1,91 @@
+/*
+ * 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<String, SemanticQuestion> 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");

Review Comment:
   💡 **Threshold parsing:** `Double.parseDouble()` on the threshold/uncertainty 
strings will throw `NumberFormatException` for non-numeric input like `"abc"`, 
which would bubble up as-is rather than as a clean `IllegalArgumentException`. 
The `SemanticQuestion` constructor does validate `Double.isFinite()` (catching 
`NaN` from `Double.parseDouble("NaN")`), and the test suite covers 
`threshold="NaN"` → `"within [0,1]"`. However, truly non-numeric strings would 
produce a less informative `NumberFormatException`. Consider wrapping with a 
try-catch:
   
   ```suggestion
                   definition.getThreshold() == null ? 0.5 : 
parseDouble(definition.getThreshold(), "threshold"),
                   definition.getUncertainty() == null ? 0 : 
parseDouble(definition.getUncertainty(), "uncertainty"),
   ```
   
   With a helper:
   ```java
   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);
       }
   }
   ```
   
   This would produce `"Invalid semantic question 'q': threshold must be a 
valid number: abc"` instead of `NumberFormatException: For input string: "abc"`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to