Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -568,10 +568,11 @@ static String listHint(Object bean, String methodName) {
String type = bean instanceof List ? "a List" : bean instanceof Object[] ? "an array" : "a Collection";
boolean field = methodName != null && FIELD_NAME.matcher(methodName).matches();
String name = field ? methodName : "field";
// the lookup by a field first: given [0] first, models wrote ${body[0].orderId} and took the first element
return " (" + type + " has no " + (field ? "method or key " + methodName : "such method")
+ ": pick an element first, such as [0]." + name + ", or the element whose " + name
+ " has a given value with jsonpath, such as jsonpath: \"$[?(@." + name + " == '${header." + name
+ "}')]\" with resultType java.util.Map, or split it)";
+ ": to pick the element whose " + name + " has a given value, use jsonpath: \"$[?(@." + name
+ " == '${header." + name + "}')]\" with resultType java.util.Map (the element, or null when none"
+ " matches); [0]." + name + " reads the first element; or split it)";
}

private static final Pattern FIELD_NAME = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,11 @@ public void testOgnlDotOnAListSaysHowToGetToAnElement() {
Exception e = assertThrows(Exception.class,
() -> context.resolveLanguage("simple").createExpression("${body.orderId}").evaluate(exchange,
String.class));
assertThat(e.getMessage()).contains("(a List has no method or key orderId: pick an element first, such as"
+ " [0].orderId, or the element whose orderId has a given value with jsonpath,"
+ " such as jsonpath: \"$[?(@.orderId == '${header.orderId}')]\" with"
+ " resultType java.util.Map, or split it)");
// the lookup by a field first: given [0] first, models took the first element
assertThat(e.getMessage()).contains("(a List has no method or key orderId: to pick the element whose orderId"
+ " has a given value, use jsonpath: \"$[?(@.orderId == '${header.orderId}')]\""
+ " with resultType java.util.Map (the element, or null when none matches);"
+ " [0].orderId reads the first element; or split it)");
}

@Test
Expand All @@ -254,7 +255,8 @@ public void testAMadeUpMethodOnAListSaysHowToGetToAnElement() {
Exception e = assertThrows(Exception.class,
() -> context.resolveLanguage("simple").createExpression("${body.find(o -> o.orderId == 1)}")
.evaluate(exchange, String.class));
assertThat(e.getMessage()).contains("(a List has no such method: pick an element first, such as [0].field");
assertThat(e.getMessage()).contains("(a List has no such method: to pick the element whose field has a given"
+ " value, use jsonpath");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1089,6 +1089,12 @@ public static JsonObject find(CamelCatalog catalog, String term, String kind, in
} else {
result.put("message", "Best match first; camel_catalog_doc gives the options of one");
}
// a header name models write for one that has another name, such as CamelHttpStatusCode
String wrong = HeaderChecks.WRONG_NAMES.get(term.trim());
if (wrong != null) {
result.put("header", "Camel has no header " + term.trim() + ": " + wrong);
result.put("message", result.getString("message") + "; " + result.getString("header"));
}
return result;
}

Expand Down Expand Up @@ -1679,6 +1685,15 @@ static String usage(ComponentModel model) {
return answer;
}

/**
* How to pick the one element of a JSON list by a field: the answer of camel_catalog_doc jsonpath says it first.
*/
static final String JSONPATH_ONE_ELEMENT = "to pick the one element of a list by a field, write a filter with"
+ " resultType: java.util.Map, such as expression: \"$[?(@.orderId =="
+ " '${header.id}')]\" and resultType: java.util.Map: the element when one"
+ " matches, null when none does (an index after the filter gives an empty"
+ " list); camel_catalog_sample jsonpath shows it in a route";

/** How many options of a component are listed in full when no filter is given (CAMEL-25541). */
static final int DEFAULT_OPTIONS_LIMIT = 20;

Expand Down Expand Up @@ -2322,6 +2337,10 @@ private static JsonObject languageDoc(
if ("simple".equals(model.getName()) || "csimple".equals(model.getName())) {
result.put("syntax", SIMPLE_SYNTAX);
}
if ("jsonpath".equals(model.getName())) {
// the one element of a list by a field, which models wrote as $[?(...)][0], an empty list
result.put("usage", JSONPATH_ONE_ELEMENT);
}
JsonObject script = scriptVariables(model.getName());
if (script != null) {
// the variables the script sees, and how to reach the Camel API from them
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,25 @@ public static JsonObject evaluate(ToolContext ctx, String language, String expre
result.put("evaluatedIn", "a local scratch context (no integration selected)");
evaluateLocally(ctx, lang, expression, body, predicate, result);
}
String filter = filterNote(lang, expression, result.getString("status"));
if (filter != null) {
result.put("note", result.containsKey("note") ? result.getString("note") + "; " + filter : filter);
}
return result;
}

/**
* A jsonpath filter gives a list, also for one match: models picked the element with an index after the filter,
* which gives an empty list, or a made-up syntax. In a route, resultType java.util.Map gives the one element.
*/
static String filterNote(String lang, String expression, String status) {
if (!"jsonpath".equals(lang) || expression == null || !expression.contains("[?(") || !"ok".equals(status)) {
return null;
}
return "a filter gives a list, also for one match; in a route, resultType: java.util.Map gives the one element"
+ " it matches (null when none does), and an index after the filter gives an empty list";
}

private static void evaluateInProcess(
ToolContext ctx, String lang, String expression, String body, boolean predicate, JsonObject result) {
String raw = ctx.executeAction("eval", root -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
public final class JsonPathChecks {

private static final Pattern COMPARISON = Pattern.compile(" (==|!=|<=|>=|<|>|=~|in|nin|size|empty) ");
// an index after a filter: $[?(@.orderId == 'ORD-1')][0]
private static final Pattern INDEX_AFTER_FILTER = Pattern.compile("\\[\\?\\(.*\\)]\\s*\\[\\s*-?\\d+\\s*]");
private static final String ONE_ELEMENT = "set resultType: java.util.Map (the element when one matches, null when"
+ " none does)";

private JsonPathChecks() {
}
Expand Down Expand Up @@ -67,7 +71,15 @@ public static List<String> validateYamlJsonPath(String content, CamelCatalog cat
break;
}
}
if (text == null || text.isEmpty() || text.contains("[?(") || !COMPARISON.matcher(text).find()) {
if (text == null || text.isEmpty()) {
continue;
}
String filter = filterProblem(text);
if (filter != null) {
errors.add("Line " + lineNum + ": " + filter);
continue;
}
if (text.contains("[?(") || !COMPARISON.matcher(text).find()) {
continue;
}
if (SimpleChecks.isPredicate(catalog, lines, i, countLeadingSpaces(line))) {
Expand All @@ -86,4 +98,51 @@ public static List<String> validateYamlJsonPath(String content, CamelCatalog cat
}
return errors;
}

/**
* What is wrong around the filter of a path that should give the one element it matches. Jayway applies an index
* after a filter to each element the filter matched, so $[?(@.orderId == 'ORD-1')][0] gives an empty list; and
* &amp;&amp; or || outside the filter is no JsonPath (500 in a REST route). The one element comes with resultType
* java.util.Map: a single match is the element, and no match null.
*/
static String filterProblem(String text) {
if (!text.contains("[?(")) {
return null;
}
if (INDEX_AFTER_FILTER.matcher(text).find()) {
return "an index after a filter gives an empty list in JsonPath ([?(...)][0] applies the index to each element"
+ " the filter matched): to get the one element, leave the index out and " + ONE_ELEMENT;
}
String outside = outsideFilters(text);
if (outside.contains("&&") || outside.contains("||")) {
return "&& and || work inside a filter only, such as $[?(@.status == 'paid' && @.qty > 0)]; to test whether"
+ " a filter matched, " + ONE_ELEMENT + ", and check ${body} == null in a choice";
}
return null;
}

// the text with each filter [?(...)] taken out
private static String outsideFilters(String text) {
StringBuilder sb = new StringBuilder();
int i = 0;
while (i < text.length()) {
if (text.startsWith("[?(", i)) {
int depth = 0;
int j = i + 2;
for (; j < text.length(); j++) {
char c = text.charAt(j);
if (c == '(') {
depth++;
} else if (c == ')' && --depth == 0) {
break;
}
}
// past the closing )]
i = Math.min(text.length(), j + 2);
} else {
sb.append(text.charAt(i++));
}
}
return sb.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,23 @@ void aBrokerWithoutAComponentOfItsOwnFindsJms() {
}
}

/** The one element of a JSON list by a field: models wrote $[?(...)][0], an empty list (the r20 REST runs). */
@Test
void theJsonPathAnswerSaysHowToPickOneElement() throws Exception {
JsonObject doc = catalogDoc(Map.of("name", "jsonpath"));
assertTrue(doc.getString("usage").contains("resultType: java.util.Map"), doc.getString("usage"));
assertTrue(doc.getString("usage").contains("an index after the filter gives an empty list"), doc.getString("usage"));
}

@Test
void aWrongHeaderNameIsAnsweredWithTheRightOne() {
org.apache.camel.catalog.CamelCatalog catalog = new org.apache.camel.catalog.DefaultCamelCatalog();
JsonObject result = CatalogDocs.find(catalog, "CamelHttpStatusCode", null, 10);
assertTrue(result.getString("header").startsWith("Camel has no header CamelHttpStatusCode"), result.toJson());
assertTrue(result.getString("header").contains("CamelHttpResponseCode (Exchange.HTTP_RESPONSE_CODE)"));
assertNull(CatalogDocs.find(catalog, "CamelHttpResponseCode", null, 10).get("header"));
}

@Test
void anInterfaceNameFindsTheBuiltInBeans() {
org.apache.camel.catalog.CamelCatalog catalog = new org.apache.camel.catalog.DefaultCamelCatalog();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ class ExpressionEvaluatorLanguageTest {

private static final String STOCK = "[{\"sku\":\"CAMEL-MUG\",\"qty\":42},{\"sku\":\"CAMEL-CAP\",\"qty\":0}]";

/** A jsonpath filter gives a list: the answer says how a route gets the one element (r20 REST runs). */
@Test
void aJsonPathFilterSaysHowToGetTheOneElement() {
assertThat(ExpressionEvaluator.filterNote("jsonpath", "$[?(@.sku == 'CAMEL-MUG')]", "ok"))
.contains("resultType: java.util.Map gives the one element").contains("an index after the filter");
assertThat(ExpressionEvaluator.filterNote("jsonpath", "$.sku", "ok")).isNull();
assertThat(ExpressionEvaluator.filterNote("simple", "${body}", "ok")).isNull();
assertThat(ExpressionEvaluator.filterNote("jsonpath", "$[?(@.sku == 'X')", "error")).isNull();
}

@Test
void aLanguageThatCannotBeLoadedSaysWhatToDo() {
JsonObject result = ExpressionEvaluator.evaluate(new ToolContext(), "no-such-language", "x", null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,60 @@ void comparisonAsAnExpression() {
.contains("read as $[?(@.status == 'paid')]");
}

/** The one order of a list by its id (the REST example of the r20 runs): an index after the filter gives []. */
@Test
void anIndexAfterAFilterGivesAnEmptyList() {
String yaml = """
- route:
from:
uri: direct:get-order
steps:
- setBody:
expression:
jsonpath:
expression: "$[?(@.orderId == '${header.id}')][0]"
""";
List<String> errors = JsonPathChecks.validateYamlJsonPath(yaml, catalog);
assertThat(errors).singleElement().satisfies(e -> assertThat(e)
.startsWith("Line 8: an index after a filter gives an empty list in JsonPath")
.contains("leave the index out and set resultType: java.util.Map"));
}

@Test
void andOutsideAFilterIsNoJsonPath() {
String yaml = """
- route:
from:
uri: direct:get-order
steps:
- choice:
when:
- expression:
jsonpath:
expression: "$[?(@.orderId == '${header.id}')] && size() > 0"
""";
List<String> errors = JsonPathChecks.validateYamlJsonPath(yaml, catalog);
assertThat(errors).singleElement().satisfies(e -> assertThat(e)
.startsWith("Line 9: && and || work inside a filter only")
.contains("set resultType: java.util.Map").contains("check ${body} == null"));
}

@Test
void theOneElementRecipeIsFine() {
String yaml = """
- route:
from:
uri: direct:get-order
steps:
- setBody:
expression:
jsonpath:
expression: "$[?(@.orderId == '${header.id}' && @.status == 'paid')]"
resultType: java.util.Map
""";
assertThat(JsonPathChecks.validateYamlJsonPath(yaml, catalog)).isEmpty();
}

@Test
void nestedPathHasNoSimpleForm() {
String yaml = """
Expand Down
Loading