diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java index 0960cdb14c739..6fc97eedcc480 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java @@ -57,6 +57,15 @@ private HeaderChecks() { */ static final Map METADATA_OPTION = Map.of("timer", "includeMetadata"); + /** + * A comparison of the timer's counter with a number: the counter counts the fires from 1, so {@code == 0} is never + * true (CAMEL-25744: in 8 of 10 benchmark runs a model compared it with 0 to pick the first fire). The Simple forms + * (${exchangeProperty.CamelTimerCounter} == 0, ${header.CamelTimerCounter == 0 ? ...}) and the Groovy ones + * (exchange.getProperty('CamelTimerCounter', Integer) == 0). + */ + static final Pattern TIMER_COUNTER_COMPARISON = Pattern.compile( + "CamelTimerCounter(?:\\}|['\"]\\s*(?:,\\s*[\\w.]+\\s*)?\\))?\\s*(==|!=|<=|<|>=|>)\\s*['\"]?(\\d+)['\"]?"); + static final Pattern EXCHANGE_PROPERTY_REF_PATTERN = Pattern.compile("\\$\\{exchangeProperty\\.(Camel[A-Z][A-Za-z0-9]*)"); /** Whether the option that makes the component set its exchange properties is true somewhere in the content. */ @@ -106,11 +115,36 @@ static String intentHint(String name) { return null; } + /** + * The comparisons of CamelTimerCounter that can never be true (== 0, <= 0, < 1) or never be false (!= 0, > 0, >= + * 1), as the counter is 1 on the first fire. + */ + static List timerCounterComparisons(String content) { + List msgs = new ArrayList<>(); + String[] lines = content.split("\n", -1); + for (int i = 0; i < lines.length; i++) { + Matcher m = TIMER_COUNTER_COMPARISON.matcher(lines[i]); + while (m.find()) { + String op = m.group(1); + int n = Integer.parseInt(m.group(2)); + boolean never = ("==".equals(op) || "<=".equals(op)) && n == 0 || "<".equals(op) && n <= 1; + boolean always = ("!=".equals(op) || ">".equals(op)) && n == 0 || ">=".equals(op) && n <= 1; + if (never || always) { + msgs.add("Line " + (i + 1) + ": CamelTimerCounter counts the fires from 1 (the first fire is 1), so " + + op + " " + n + " is " + (never ? "never true" : "always true") + + ": compare with 1 to pick the first fire"); + } + } + } + return msgs; + } + public static List validateKnownHeaders(String content, CamelCatalog catalog) { List msgs = new ArrayList<>(); if (content == null) { return msgs; } + msgs.addAll(timerCounterComparisons(content)); Set known = new LinkedHashSet<>(); Map owner = new LinkedHashMap<>(); Matcher sm = SCHEME_IN_URI_PATTERN.matcher(content); diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java index afaa47ac342c1..b64b7f94305e1 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java @@ -457,6 +457,65 @@ void aTimerExchangePropertyIsSetOnlyWithIncludeMetadata() { assertThat(msgs.get(1)).startsWith("Line 4:").contains("timer sets CamelTimerName only with includeMetadata=true"); } + /** CAMEL-25744: the counter is 1 on the first fire, so a comparison with 0 never picks it. */ + @Test + void aTimerCounterComparisonThatCanNeverBeTrueIsReported() { + List msgs = SourceValidator.validateKnownHeaders(""" + - from: + uri: "timer:tick?repeatCount=2&includeMetadata=true" + steps: + - setBody: + simple: "resource:file:${exchangeProperty.CamelTimerCounter == 0 ? 'a.json' : 'b.json'}" + - choice: + when: + - simple: "${header.CamelTimerCounter} != 0" + steps: + - log: "${body}" + - filter: + groovy: "exchange.getProperty('CamelTimerCounter', Integer) < 1" + """, catalog); + assertThat(msgs).hasSize(3); + assertThat(msgs.get(0)).startsWith("Line 5:").contains("counts the fires from 1").contains("== 0 is never true") + .contains("compare with 1"); + assertThat(msgs.get(1)).startsWith("Line 8:").contains("!= 0 is always true"); + assertThat(msgs.get(2)).startsWith("Line 12:").contains("< 1 is never true"); + } + + @Test + void aTimerCounterComparisonThatIsAlwaysTrueIsReported() { + List msgs = SourceValidator.validateKnownHeaders(""" + - from: + uri: "timer:tick?repeatCount=2&includeMetadata=true" + steps: + - filter: + simple: "${exchangeProperty.CamelTimerCounter} > 0" + - filter: + groovy: "exchange.getProperty('CamelTimerCounter', Integer) >= 1" + - filter: + simple: "${exchangeProperty.CamelTimerCounter} >= 0" + """, catalog); + assertThat(msgs).hasSize(3); + assertThat(msgs.get(0)).startsWith("Line 5:").contains("> 0 is always true"); + assertThat(msgs.get(1)).startsWith("Line 7:").contains(">= 1 is always true"); + assertThat(msgs.get(2)).startsWith("Line 9:").contains(">= 0 is always true"); + } + + @Test + void aTimerCounterComparedWithOneIsFine() { + List msgs = SourceValidator.validateKnownHeaders(""" + - from: + uri: "timer:tick?repeatCount=2&includeMetadata=true" + steps: + - setBody: + simple: "resource:file:${exchangeProperty.CamelTimerCounter == 1 ? 'a.json' : 'b.json'}" + - filter: + simple: "${exchangeProperty.CamelTimerCounter} > 1" + - filter: + simple: "${exchangeProperty.CamelTimerCounter} >= 2" + """, catalog); + assertThat(msgs).isEmpty(); + } + @Test void withIncludeMetadataAHeaderLookupFindsTheExchangeProperty() { // Simple's ${header.x} falls back to the exchange properties, so it reads the counter