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("""