This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 17e6c5996202 CAMEL-24839, CAMEL-24840, CAMEL-24841: a file endpoint on 
a regular file says so, a property list item says key and value, and a jsonpath 
comparison works as a condition and says what it is elsewhere (#26621)
17e6c5996202 is described below

commit 17e6c59962022f88f4b8137493bdd9b961b174ab
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Sep 20 12:42:11 2026 +0200

    CAMEL-24839, CAMEL-24840, CAMEL-24841: a file endpoint on a regular file 
says so, a property list item says key and value, and a jsonpath comparison 
works as a condition and says what it is elsewhere (#26621)
    
    CAMEL-24839: the file consumer is refused when its directory is an existing 
regular file, naming file:.?fileName=... CAMEL-24840: restConfiguration's 
key/value property lists say the item form. CAMEL-24841: the easy predicate 
parser accepts the comparison written with its root ($.status == 'paid' -> 
$[?(@.status == 'paid')]); where a path is expected the runtime message and a 
new validator check say it is a comparison and give the filter form, and the 
Simple form for a top-level field [...]
---
 .../apache/camel/component/file/FileEndpoint.java  |  10 ++
 .../apache/camel/jsonpath/JsonPathExpression.java  |  29 ++++-
 .../easypredicate/EasyPredicateOperators.java      |   8 +-
 .../easypredicate/EasyPredicateParser.java         |  19 +++-
 .../camel/jsonpath/JsonPathComparisonHintTest.java |  50 +++++++++
 .../EasyJsonPathWithRootPathCBRTest.java           |  74 +++++++++++++
 .../easypredicate/EasyPredicateParserTest.java     |  46 ++++++++
 .../file/FileConsumeFileAsDirectoryTest.java       |  64 +++++++++++
 .../dsl/jbang/core/commands/ai/JsonPathChecks.java |  89 ++++++++++++++++
 .../jbang/core/commands/ai/SourceValidator.java    |   1 +
 .../jbang/core/commands/ai/JsonPathChecksTest.java | 117 +++++++++++++++++++++
 .../camel/dsl/yaml/validator/SchemaHints.java      |   6 ++
 .../validator/YamlValidatorPropertyHintTest.java   |  14 +++
 13 files changed, 521 insertions(+), 6 deletions(-)

diff --git 
a/components/camel-file/src/main/java/org/apache/camel/component/file/FileEndpoint.java
 
b/components/camel-file/src/main/java/org/apache/camel/component/file/FileEndpoint.java
index 5c78164127b9..621e144f76cd 100644
--- 
a/components/camel-file/src/main/java/org/apache/camel/component/file/FileEndpoint.java
+++ 
b/components/camel-file/src/main/java/org/apache/camel/component/file/FileEndpoint.java
@@ -101,6 +101,16 @@ public class FileEndpoint extends 
GenericFileEndpoint<File> {
         ObjectHelper.notNull(operations, PARAM_OPERATIONS);
         ObjectHelper.notNull(file, "file");
 
+        if (file.isFile()) {
+            // file:order.json: the file component reads the files of a 
directory; a regular file as the directory
+            // polled nothing and said nothing (CAMEL-24839)
+            String parent = file.getParent() != null ? file.getParent() : ".";
+            throw new IllegalArgumentException(
+                    file + " is a file, not a directory: the file component 
reads the files of a directory."
+                                               + " To read this one file use 
file:" + parent + "?fileName=" + file.getName()
+                                               + " (as from:, or with 
pollEnrich)");
+        }
+
         FileConsumer result = newFileConsumer(processor, operations);
 
         if (isDelete() && getMove() != null) {
diff --git 
a/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathExpression.java
 
b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathExpression.java
index 0a94ccacf0a7..0dbbe5109299 100644
--- 
a/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathExpression.java
+++ 
b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/JsonPathExpression.java
@@ -28,6 +28,7 @@ import org.apache.camel.Exchange;
 import org.apache.camel.Expression;
 import org.apache.camel.ExpressionEvaluationException;
 import org.apache.camel.ExpressionIllegalSyntaxException;
+import org.apache.camel.jsonpath.easypredicate.EasyPredicateOperators;
 import org.apache.camel.jsonpath.easypredicate.EasyPredicateParser;
 import org.apache.camel.support.ExpressionAdapter;
 import org.slf4j.Logger;
@@ -190,6 +191,12 @@ public class JsonPathExpression extends ExpressionAdapter {
             }
             return 
exchange.getContext().getTypeConverter().convertTo(resultType, exchange, 
result);
         } catch (Exception e) {
+            // the path is compiled on the first evaluation: a comparison 
written on it fails here with the Jayway
+            // message about blank characters; say what it is and what to 
write (CAMEL-24841)
+            String hint = e instanceof 
com.jayway.jsonpath.InvalidPathException ? comparisonHint(expression) : null;
+            if (hint != null) {
+                throw new ExpressionEvaluationException(this, e.getMessage() + 
" (" + hint + ")", exchange, e);
+            }
             throw new ExpressionEvaluationException(this, exchange, e);
         }
     }
@@ -212,8 +219,28 @@ public class JsonPathExpression extends ExpressionAdapter {
             engine = new JsonPathEngine(
                     exp, source, writeAsString, suppressExceptions, 
allowSimple, options, context);
         } catch (Exception e) {
-            throw new ExpressionIllegalSyntaxException(exp, e);
+            String hint = comparisonHint(exp);
+            throw new ExpressionIllegalSyntaxException(hint != null ? exp + " 
(" + hint + ")" : exp, e);
+        }
+    }
+
+    /**
+     * The hint for a comparison written on the path ($.status == 'paid') 
where a path is expected: as a predicate it is
+     * read as a condition (the easy predicate syntax); as an expression it is 
not a path (CAMEL-24841).
+     */
+    static String comparisonHint(String exp) {
+        if (exp == null || exp.contains("[?(") || 
!EasyPredicateOperators.hasOperator(exp)) {
+            return null;
         }
+        String field = exp.startsWith("$.") ? exp.substring(2) : 
exp.startsWith("$") ? exp.substring(1) : exp;
+        int sp = field.indexOf(' ');
+        String left = sp > 0 ? field.substring(0, sp) : field;
+        String rest = sp > 0 ? field.substring(sp) : "";
+        // ${body[key]} reads one key of a Map: the Simple form is offered for 
a top-level field only
+        String simple = left.contains(".")
+                ? "" : "; to keep the value use the path $." + left + " and 
compare it in simple, ${body[" + left + "]}" + rest;
+        return "jsonpath is a path, not a comparison: as a condition (when, 
filter) write " + exp
+               + " or $[?(@." + left + rest + ")]" + simple;
     }
 
     @Override
diff --git 
a/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/easypredicate/EasyPredicateOperators.java
 
b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/easypredicate/EasyPredicateOperators.java
index 1e4607bed5ed..b8fcf76ea3d8 100644
--- 
a/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/easypredicate/EasyPredicateOperators.java
+++ 
b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/easypredicate/EasyPredicateOperators.java
@@ -44,7 +44,13 @@ public final class EasyPredicateOperators {
     /**
      * Does the expression have any operator (with single space around)?
      */
-    static boolean hasOperator(String exp) {
+    /**
+     * Does the expression have any operator (with a single space around it on 
each side)?
+     *
+     * @param  exp the expression to inspect
+     * @return     true if the expression contains a recognised operator 
surrounded by spaces
+     */
+    public static boolean hasOperator(String exp) {
         // need to have space around operator to not match eg in used in some 
other word
         return Arrays.stream(OPS).anyMatch(o -> exp.contains(" " + o));
     }
diff --git 
a/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/easypredicate/EasyPredicateParser.java
 
b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/easypredicate/EasyPredicateParser.java
index 850161b77e56..b23f113f6b2b 100644
--- 
a/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/easypredicate/EasyPredicateParser.java
+++ 
b/components/camel-jsonpath/src/main/java/org/apache/camel/jsonpath/easypredicate/EasyPredicateParser.java
@@ -38,9 +38,19 @@ public class EasyPredicateParser {
      */
     public String parse(String predicate) {
 
+        boolean rooted = false;
         if (predicate.startsWith("$")) {
-            // regular json path so skip
-            return predicate;
+            if (predicate.contains("[?(") || !hasOperator(predicate)) {
+                // regular json path (a filter, or a plain path) so skip
+                return predicate;
+            }
+            // $.status == 'paid': the easy form written with its root, the 
way a path is written (CAMEL-24841);
+            // the filter goes on the parent of the field, the root here
+            rooted = true;
+            predicate = predicate.substring(1);
+            if (predicate.startsWith(".")) {
+                predicate = predicate.substring(1);
+            }
         }
 
         // must have an operator
@@ -63,14 +73,15 @@ public class EasyPredicateParser {
                 String after;
                 int pos = prev.lastIndexOf('.');
                 if (pos == -1) {
-                    before = "..*";
+                    // without a root the field is looked for anywhere ($..*), 
with a root ($.status) at the root
+                    before = rooted ? "" : "..*";
                     after = prev;
                 } else {
                     before = prev.substring(0, pos);
                     after = prev.substring(pos + 1);
                 }
                 sb.append("$");
-                if (!before.startsWith(".")) {
+                if (!before.isEmpty() && !before.startsWith(".")) {
                     sb.append(".");
                 }
                 sb.append(before);
diff --git 
a/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathComparisonHintTest.java
 
b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathComparisonHintTest.java
new file mode 100644
index 000000000000..1ebadd24cdee
--- /dev/null
+++ 
b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/JsonPathComparisonHintTest.java
@@ -0,0 +1,50 @@
+/*
+ * 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.jsonpath;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The hint for a comparison written where a path is expected (CAMEL-24841).
+ */
+public class JsonPathComparisonHintTest {
+
+    @Test
+    public void testTopLevelFieldOffersTheSimpleForm() {
+        String hint = JsonPathExpression.comparisonHint("$.status == 'paid'");
+        assertTrue(hint.contains("$[?(@.status == 'paid')]"), hint);
+        assertTrue(hint.contains("${body[status]} == 'paid'"), hint);
+    }
+
+    @Test
+    public void testNestedPathHasNoSimpleForm() {
+        // ${body[key]} reads one key of a Map, so the Simple form is not 
offered for a nested path
+        String hint = JsonPathExpression.comparisonHint("$.store.book.price < 
10");
+        assertTrue(hint.contains("$[?(@.store.book.price < 10)]"), hint);
+        assertFalse(hint.contains("${body["), hint);
+    }
+
+    @Test
+    public void testPathsAndFiltersHaveNoHint() {
+        assertNull(JsonPathExpression.comparisonHint("$.status"));
+        assertNull(JsonPathExpression.comparisonHint("$.store.book[?(@.price < 
10)]"));
+    }
+}
diff --git 
a/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/easypredicate/EasyJsonPathWithRootPathCBRTest.java
 
b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/easypredicate/EasyJsonPathWithRootPathCBRTest.java
new file mode 100644
index 000000000000..deea9dcf01ed
--- /dev/null
+++ 
b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/easypredicate/EasyJsonPathWithRootPathCBRTest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.jsonpath.easypredicate;
+
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A condition written on the path, $.status == 'paid', works as a predicate 
(CAMEL-24841).
+ */
+public class EasyJsonPathWithRootPathCBRTest extends CamelTestSupport {
+
+    @Override
+    protected RouteBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from("direct:start")
+                        .choice()
+                        .when().jsonpath("$.status == 'paid'")
+                        .to("mock:paid")
+                        .otherwise()
+                        .to("mock:other");
+            }
+        };
+    }
+
+    @Test
+    public void testPaid() throws Exception {
+        getMockEndpoint("mock:paid").expectedMessageCount(1);
+        getMockEndpoint("mock:other").expectedMessageCount(1);
+
+        template.sendBody("direct:start", "{\"orderId\": \"ORD-1001\", 
\"status\": \"paid\"}");
+        template.sendBody("direct:start", "{\"orderId\": \"ORD-1003\", 
\"status\": \"pending\"}");
+
+        MockEndpoint.assertIsSatisfied(context);
+    }
+
+    @Test
+    public void testAsAnExpressionTheHintSaysSo() {
+        // an expression is a path; the comparison written on it is not, and 
the message says what to write
+        Exception e = assertThrows(Exception.class, () -> {
+            org.apache.camel.Expression exp = 
context.resolveLanguage("jsonpath").createExpression("$.status == 'paid'");
+            exp.init(context);
+            org.apache.camel.Exchange exchange = new 
org.apache.camel.support.DefaultExchange(context);
+            exchange.getMessage().setBody("{\"status\": \"paid\"}");
+            exp.evaluate(exchange, Object.class);
+        });
+        String msg = e.getMessage() + " " + (e.getCause() != null ? 
e.getCause().getMessage() : "");
+        assertTrue(msg.contains("jsonpath is a path, not a comparison"), msg);
+        assertTrue(msg.contains("$[?(@.status == 'paid')]"), msg);
+        assertTrue(msg.contains("${body[status]} == 'paid'"), msg);
+    }
+
+}
diff --git 
a/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/easypredicate/EasyPredicateParserTest.java
 
b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/easypredicate/EasyPredicateParserTest.java
new file mode 100644
index 000000000000..5ff2a3ccda97
--- /dev/null
+++ 
b/components/camel-jsonpath/src/test/java/org/apache/camel/jsonpath/easypredicate/EasyPredicateParserTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.jsonpath.easypredicate;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class EasyPredicateParserTest {
+
+    private final EasyPredicateParser parser = new EasyPredicateParser();
+
+    @Test
+    public void testWithoutRoot() {
+        assertEquals("$..*[?(@.price < 10)]", parser.parse("price < 10"));
+        assertEquals("$.store.book[?(@.price < 10)]", 
parser.parse("store.book.price < 10"));
+    }
+
+    @Test
+    public void testWithRoot() {
+        // CAMEL-24841: the comparison written on the path, the way people 
write it first
+        assertEquals("$[?(@.status == 'paid')]", parser.parse("$.status == 
'paid'"));
+        assertEquals("$.store.book[?(@.price < 10)]", 
parser.parse("$.store.book.price < 10"));
+    }
+
+    @Test
+    public void testRegularPathsAreKept() {
+        assertEquals("$.status", parser.parse("$.status"));
+        assertEquals("$..book[?(@.price < 10)]", 
parser.parse("$..book[?(@.price < 10)]"));
+        assertEquals("$[?(@.status == 'paid')]", parser.parse("$[?(@.status == 
'paid')]"));
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumeFileAsDirectoryTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumeFileAsDirectoryTest.java
new file mode 100644
index 000000000000..2fa7c1938a34
--- /dev/null
+++ 
b/core/camel-core/src/test/java/org/apache/camel/component/file/FileConsumeFileAsDirectoryTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.component.file;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.UUID;
+
+import org.apache.camel.ContextTestSupport;
+import org.apache.camel.builder.RouteBuilder;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * A file endpoint whose directory is an existing regular file used to start 
and stay silent; it fails with a message
+ * that says what to write (CAMEL-24839).
+ */
+public class FileConsumeFileAsDirectoryTest extends ContextTestSupport {
+
+    private static final String TEST_FILE_NAME = "as-directory-" + 
UUID.randomUUID() + ".json";
+    private final Path file = Path.of(TEST_FILE_NAME);
+
+    @Override
+    public boolean isUseRouteBuilder() {
+        return false;
+    }
+
+    @AfterEach
+    public void deleteFile() throws Exception {
+        Files.deleteIfExists(file);
+    }
+
+    @Test
+    public void testConsumeFromAFile() throws Exception {
+        Files.writeString(file, "{}");
+
+        context.addRoutes(new RouteBuilder() {
+            public void configure() {
+                from("file:" + TEST_FILE_NAME + 
"?initialDelay=0&delay=10&noop=true").to("mock:result");
+            }
+        });
+        Exception e = assertThrows(Exception.class, () -> context.start());
+        String msg = e.getMessage() + (e.getCause() != null ? 
e.getCause().getMessage() : "");
+        assertTrue(msg.contains(TEST_FILE_NAME + " is a file, not a 
directory"), msg);
+        assertTrue(msg.contains("file:.?fileName=" + TEST_FILE_NAME), msg);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecks.java
new file mode 100644
index 000000000000..d12a0becc80a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecks.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import org.apache.camel.catalog.CamelCatalog;
+
+import static 
org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.countLeadingSpaces;
+import static 
org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.extractYamlValue;
+
+/**
+ * Checks on the jsonpath expressions of a YAML route. A comparison written on 
the path ($.status == 'paid') is a
+ * condition in a when or filter (the easy predicate syntax, CAMEL-24841) and 
not a path anywhere else: there the
+ * runtime fails with a Jayway message about blank characters, and the check 
says what to write instead.
+ */
+public final class JsonPathChecks {
+
+    private static final Pattern COMPARISON = Pattern.compile(" 
(==|!=|<=|>=|<|>|=~|in|nin|size|empty) ");
+
+    private JsonPathChecks() {
+    }
+
+    public static List<String> validateYamlJsonPath(String content, 
CamelCatalog catalog) {
+        List<String> errors = new ArrayList<>();
+        if (content == null || content.isBlank()) {
+            return errors;
+        }
+        String[] lines = content.split("\n", -1);
+        for (int i = 0; i < lines.length; i++) {
+            String line = lines[i];
+            if (line.isBlank() || line.trim().startsWith("#")) {
+                continue;
+            }
+            String trimmed = line.trim();
+            String key = trimmed.startsWith("- ") ? trimmed.substring(2) : 
trimmed;
+            String text = null;
+            int lineNum = i + 1;
+            if (key.startsWith("jsonpath:") && !key.equals("jsonpath:")) {
+                text = extractYamlValue(key, "jsonpath");
+            } else if (key.equals("jsonpath:")) {
+                for (int j = i + 1; j < lines.length; j++) {
+                    String next = lines[j].trim();
+                    if (next.isBlank()) {
+                        continue;
+                    }
+                    if (next.startsWith("expression:")) {
+                        text = extractYamlValue(next, "expression");
+                        lineNum = j + 1;
+                    }
+                    break;
+                }
+            }
+            if (text == null || text.isEmpty() || text.contains("[?(") || 
!COMPARISON.matcher(text).find()) {
+                continue;
+            }
+            if (SimpleChecks.isPredicate(catalog, lines, i, 
countLeadingSpaces(line))) {
+                // a condition: the easy predicate syntax reads it as 
$[?(@.status == 'paid')]
+                continue;
+            }
+            String field = text.startsWith("$.") ? text.substring(2) : 
text.startsWith("$") ? text.substring(1) : text;
+            int sp = field.indexOf(' ');
+            String left = sp > 0 ? field.substring(0, sp) : field;
+            String rest = sp > 0 ? field.substring(sp) : "";
+            // ${body[key]} reads one key of a Map: the Simple form is offered 
for a top-level field only
+            String simple = left.contains(".") ? "" : " and compare it in 
simple (${body[" + left + "]}" + rest + ")";
+            errors.add("Line " + lineNum + ": jsonpath is a path, not a 
comparison: here it must give a value, write the"
+                       + " path $." + left + simple + "; as a condition in a 
when or filter, " + text
+                       + " is read as $[?(@." + left + rest + ")]");
+        }
+        return errors;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
index d1be20fccf42..9d7afd0fe5ff 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
@@ -229,6 +229,7 @@ public final class SourceValidator {
         msgs.addAll(StructureChecks.validateTopLevelOrder(content));
         msgs.addAll(validateYamlEndpoints(content, catalog));
         msgs.addAll(validateYamlSimple(content, catalog));
+        msgs.addAll(JsonPathChecks.validateYamlJsonPath(content, catalog));
         msgs.addAll(validateKnownHeaders(content, catalog));
         msgs.addAll(validateBeanTypes(content));
         return msgs;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecksTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecksTest.java
new file mode 100644
index 000000000000..165b0d1cc2cc
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecksTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.util.List;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class JsonPathChecksTest {
+
+    private static CamelCatalog catalog;
+
+    @BeforeAll
+    static void loadCatalog() {
+        catalog = new DefaultCamelCatalog();
+    }
+
+    @Test
+    void comparisonAsAnExpression() {
+        // CAMEL-24841: setHeader wants a value; the comparison is a condition
+        String yaml = """
+                - route:
+                    from:
+                      uri: "timer:t?repeatCount=1"
+                      steps:
+                        - setHeader:
+                            name: paid
+                            expression:
+                              jsonpath:
+                                expression: "$.status == 'paid'"
+                """;
+        List<String> errors = JsonPathChecks.validateYamlJsonPath(yaml, 
catalog);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0)).startsWith("Line 9: jsonpath is a path, not 
a comparison")
+                .contains("write the path $.status and compare it in simple 
(${body[status]} == 'paid')")
+                .contains("read as $[?(@.status == 'paid')]");
+    }
+
+    @Test
+    void nestedPathHasNoSimpleForm() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: "timer:t?repeatCount=1"
+                      steps:
+                        - setBody:
+                            expression:
+                              jsonpath: "$.store.book.price < 10"
+                """;
+        List<String> errors = JsonPathChecks.validateYamlJsonPath(yaml, 
catalog);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0)).contains("write the path 
$.store.book.price;").doesNotContain("${body[");
+    }
+
+    @Test
+    void comparisonAsACondition() {
+        // a when or filter reads it as the easy predicate: nothing to say
+        String yaml = """
+                - route:
+                    from:
+                      uri: "timer:t?repeatCount=1"
+                      steps:
+                        - choice:
+                            when:
+                              - expression:
+                                  jsonpath:
+                                    expression: "$.status == 'paid'"
+                                steps:
+                                  - log: "paid"
+                        - filter:
+                            expression:
+                              jsonpath: "$.status == 'paid'"
+                            steps:
+                              - log: "paid"
+                """;
+        assertThat(JsonPathChecks.validateYamlJsonPath(yaml, 
catalog)).isEmpty();
+    }
+
+    @Test
+    void pathsAndFiltersAreKept() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: "timer:t?repeatCount=1"
+                      steps:
+                        - setBody:
+                            expression:
+                              jsonpath:
+                                expression: "$.lines[?(@.qty > 1)]"
+                        - split:
+                            expression:
+                              jsonpath: "$.lines[*]"
+                            steps:
+                              - log: "${body}"
+                """;
+        assertThat(JsonPathChecks.validateYamlJsonPath(yaml, 
catalog)).isEmpty();
+    }
+}
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
index 03c2bea61047..e3f6102c0171 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
@@ -326,6 +326,12 @@ final class SchemaHints {
             unknownProperty(null, m -> m.unknown().equals("bean") && 
!m.locationEndsWith("/steps"),
                     m -> "the bean language is written as method: (expression: 
{method: {ref: myBean, method:"
                          + " process}}), or call the bean as a step with - 
bean: {ref: myBean, method: process}"),
+            // dataFormatProperty: [- prettyPrint: "true"]: an item of a 
key/value property list is a key and a value
+            
unknownProperty(".*/restConfiguration/(dataFormatProperty|componentProperty|endpointProperty|consumerProperty"
+                            + "|apiProperty|corsHeaders)/\\d+",
+                    ANY,
+                    m -> "an item of " + m.parentName() + " is a key and a 
value: - key: " + m.unknown()
+                         + " followed by value: \"...\" (indented under the 
-)"),
             // setHeader: {CamelNumberA: {simple: ...}} : the name is a 
property, not the key
             
unknownProperty(".*/(setHeader|setProperty|setVariable|removeHeader|removeProperty|removeVariable)",
                     m -> YamlValidator.closest(m.unknown(), 
m.validator().knownProperties(m.schemaLocation())) == null,
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
index bd8af78f8a48..e2cf1c3e461f 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
@@ -539,6 +539,20 @@ public class YamlValidatorPropertyHintTest {
                 .contains("resilience4jConfiguration: {failureRateThreshold: 
...}"));
     }
 
+    @Test
+    public void testRestConfigurationPropertyListItemSaysKeyAndValue() throws 
Exception {
+        // CAMEL-24840: the map form already says "write it as a list"; the 
list item written as a map said nothing
+        List<Error> errors = validator.validate("""
+                - restConfiguration:
+                    component: platform-http
+                    bindingMode: json
+                    dataFormatProperty:
+                      - prettyPrint: "true"
+                """);
+        assertThat(errors).anySatisfy(e -> 
assertThat(e.getMessage()).contains("property 'prettyPrint' is not defined")
+                .contains("an item of dataFormatProperty is a key and a value: 
- key: prettyPrint followed by value:"));
+    }
+
     @Test
     public void testLogMessageAsAnExpressionMapSaysPlainString() throws 
Exception {
         List<Error> errors = validator.validate("""

Reply via email to