This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-25182-doc-java-examples in repository https://gitbox.apache.org/repos/asf/camel.git
commit 95bc4460cc700271b4a48ef25eeb7ab1b80ea415 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 30 15:08:22 2026 +0200 CAMEL-25182: Check the Java DSL examples of the documentation with the Java DSL parser - CatalogDocJavaExamplesTest reads the Java route examples of the catalog docs and the user manual with LwJavaParser and fails on DSL calls that do not exist - fix the documentation examples that call the Java DSL wrongly; the jq header input uses the source option, the keycloak policy closes with endDoTry, the xmlSecurity namespace example has a passPhrase, the removed exceptionPolicyStrategy section is gone, and the redelivery example is a route - DataFormatClause: univocityCsv(), univocityFixed(), univocityTsv() and yaml(), as the documentation of these data formats shows them - parser: char literals, Map.of/List.of/Set.of/Arrays.asList, JDK constants in snippets without imports, enum name()/toString() and TimeUnit conversions, null stands in for objects of a class the parser cannot create, model methods named configure/configuration are no longer denied - endpoint DSL: only calls whose arguments are paths and whose scheme is a known component are endpoints (and(user, admin) and stax(Record.class) are not) Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/catalog/docs/aws2-ses-component.adoc | 2 +- .../camel/catalog/docs/aws2-sqs-component.adoc | 2 +- .../catalog/docs/aws2-translate-component.adoc | 2 +- .../camel/catalog/docs/dead-letter-channel.adoc | 2 +- .../camel/catalog/docs/disruptor-component.adoc | 2 +- .../catalog/docs/google-bigquery-component.adoc | 2 +- .../camel/catalog/docs/hwcloud-dms-component.adoc | 8 +- .../camel/catalog/docs/hwcloud-iam-component.adoc | 4 +- .../camel/catalog/docs/hwcloud-obs-component.adoc | 4 +- .../org/apache/camel/catalog/docs/intercept.adoc | 2 +- .../apache/camel/catalog/docs/jdbc-component.adoc | 2 +- .../org/apache/camel/catalog/docs/jq-language.adoc | 6 +- .../camel/catalog/docs/keycloak-security.adoc | 3 +- .../camel/catalog/docs/olingo2-component.adoc | 2 +- .../camel/catalog/docs/olingo4-component.adoc | 2 +- .../apache/camel/catalog/docs/resumable-eip.adoc | 2 +- .../camel/catalog/docs/resume-strategies.adoc | 2 +- .../apache/camel/catalog/docs/seda-component.adoc | 4 +- .../apache/camel/catalog/docs/validate-eip.adoc | 2 +- .../camel/catalog/docs/xmlSecurity-dataformat.adoc | 8 +- .../src/main/docs/aws2-ses-component.adoc | 2 +- .../src/main/docs/aws2-sqs-component.adoc | 2 +- .../src/main/docs/aws2-translate-component.adoc | 2 +- .../src/main/docs/disruptor-component.adoc | 2 +- .../src/main/docs/google-bigquery-component.adoc | 2 +- .../src/main/docs/hwcloud-dms-component.adoc | 8 +- .../src/main/docs/hwcloud-iam-component.adoc | 4 +- .../src/main/docs/hwcloud-obs-component.adoc | 4 +- .../camel-jdbc/src/main/docs/jdbc-component.adoc | 2 +- components/camel-jq/src/main/docs/jq-language.adoc | 6 +- .../src/main/docs/keycloak-security.adoc | 3 +- .../src/main/docs/olingo2-component.adoc | 2 +- .../src/main/docs/olingo4-component.adoc | 2 +- .../camel-seda/src/main/docs/seda-component.adoc | 4 +- .../snakeyaml/SnakeYAMLDataFormatClauseTest.java | 49 ++++++++ .../univocity/UniVocityDataFormatClauseTest.java | 63 ++++++++++ .../src/main/docs/xmlSecurity-dataformat.adoc | 8 +- .../modules/eips/pages/dead-letter-channel.adoc | 2 +- .../main/docs/modules/eips/pages/intercept.adoc | 2 +- .../docs/modules/eips/pages/resumable-eip.adoc | 2 +- .../docs/modules/eips/pages/resume-strategies.adoc | 2 +- .../main/docs/modules/eips/pages/validate-eip.adoc | 2 +- .../org/apache/camel/builder/DataFormatClause.java | 31 +++++ .../org/apache/camel/java/in/ChainReplayer.java | 127 ++++++++++++++++++++- .../org/apache/camel/java/in/JavaChainParser.java | 10 +- .../org/apache/camel/java/in/LwJavaParserTest.java | 66 +++++++++++ .../modules/ROOT/pages/exception-advanced.adoc | 33 ------ .../ROOT/pages/exception-handling-patterns.adoc | 2 +- .../modules/ROOT/pages/exception-redelivery.adoc | 6 +- .../modules/ROOT/pages/rest-dsl-openapi.adoc | 6 +- .../modules/ROOT/pages/route-template.adoc | 2 +- .../commands/ai/CatalogEndpointDslResolver.java | 7 +- .../commands/ai/CatalogDocJavaExamplesTest.java | 124 ++++++++++++++++++++ 53 files changed, 542 insertions(+), 110 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-ses-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-ses-component.adoc index 7ab6c2dbe639..4368831ee4cb 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-ses-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-ses-component.adoc @@ -121,7 +121,7 @@ Java:: ---- from("direct:start") .setHeader(SesConstants.SUBJECT, constant("This is my subject")) - .setHeader(SesConstants.TO, constant(Collections.singletonList("[email protected]")) + .setHeader(SesConstants.TO, constant(Collections.singletonList("[email protected]"))) .setBody(constant("This is my message text.")) .to("aws2-ses://[email protected]?accessKey=xxx&secretKey=yyy"); ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-sqs-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-sqs-component.adoc index 89fa5105095b..0b355120b900 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-sqs-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-sqs-component.adoc @@ -138,7 +138,7 @@ Java:: [source,java] ---- from("aws2-sqs://MyQueue?amazonSQSClient=#client&defaultVisibilityTimeout=5000&deleteIfFiltered=false&deleteAfterRead=false") -.filter("${header.login} == true") +.filter(simple("${header.login} == true")) .setProperty("CamelAwsSqsDeleteFiltered", constant(true)) .to("mock:filter"); ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-translate-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-translate-component.adoc index 8c976e072bc7..bb6cf7ae4179 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-translate-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/aws2-translate-component.adoc @@ -84,7 +84,7 @@ Camel-AWS Translate component provides the following operation on the producer s from("direct:start") .setHeader(TranslateConstants.SOURCE_LANGUAGE, TranslateLanguageEnum.ITALIAN) .setHeader(TranslateConstants.TARGET_LANGUAGE, TranslateLanguageEnum.GERMAN) - .setBody("Ciao") + .setBody(constant("Ciao")) .to("aws2-translate://test?translateClient=#amazonTranslateClient&operation=translateText"); ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dead-letter-channel.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dead-letter-channel.adoc index 93e755f663a7..2df230324a93 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dead-letter-channel.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/dead-letter-channel.adoc @@ -324,7 +324,7 @@ Java:: ---- errorHandler(deadLetterChannel("jms:queue:dead") .maximumRedeliveries(3) - .onRedeliver(new MyOnRedeliveryProcessor()); + .onRedelivery(new MyOnRedeliveryProcessor())); ---- Spring XML:: diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/disruptor-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/disruptor-component.adoc index a864a33aff77..4fb1d1e7c904 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/disruptor-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/disruptor-component.adoc @@ -196,7 +196,7 @@ something like: ._Java-only: incomplete route (thread pool example)_ [source,java] ---- -from("disruptor:stageName").thread(5).process(...) +from("disruptor:stageName").threads(5).process(...) ---- Can wind up with adding a normal diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-bigquery-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-bigquery-component.adoc index 6685537b880a..965789ea0996 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-bigquery-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/google-bigquery-component.adoc @@ -109,7 +109,7 @@ I.e. the following route will create tables and insert records sharded on a per- [source,java] ---- from("direct:start") - .header(GoogleBigQueryConstants.TABLE_SUFFIX, "_${date:now:yyyyMMdd}") + .setHeader(GoogleBigQueryConstants.TABLE_SUFFIX, simple("_${date:now:yyyyMMdd}")) .to("google-bigquery:sampleDataset:sampleTable") ---- Note it is recommended to use partitioning for this use case. diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-dms-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-dms-component.adoc index f747c4dabadf..e1264510d92a 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-dms-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-dms-component.adoc @@ -119,7 +119,7 @@ To create an instance, you can pass the parameters through the endpoint, the exc [source,java] ---- from("direct:triggerRoute") - .setBody(new CreateInstanceRequestBody().withName("new-instance").withDescription("description").with*) // add remaining options + .setBody(constant(new CreateInstanceRequestBody().withName("new-instance").withDescription("description").with*)) // add remaining options .to("hwcloud-dms:createInstance?region=cn-north-4&accessKey=********&secretKey=********&projectId=*******") ---- @@ -130,7 +130,7 @@ Java:: [source,java] ---- from("direct:triggerRoute") - .setBody("{\"name\":\"new-instance\",\"description\":\"description\"}") // add remaining options + .setBody(constant("{\"name\":\"new-instance\",\"description\":\"description\"}")) // add remaining options .to("hwcloud-dms:createInstance?region=cn-north-4&accessKey=********&secretKey=********&projectId=*******") ---- @@ -175,7 +175,7 @@ To update an instance, you must pass the parameters through the exchange body as [source,java] ---- from("direct:triggerRoute") - .setBody(new UpdateInstanceRequestBody().withName("new-instance").withDescription("description").with*) // add remaining options + .setBody(constant(new UpdateInstanceRequestBody().withName("new-instance").withDescription("description").with*)) // add remaining options .to("hwcloud-dms:updateInstance?instanceId=******®ion=cn-north-4&accessKey=********&secretKey=********&projectId=*******") ---- @@ -186,7 +186,7 @@ Java:: [source,java] ---- from("direct:triggerRoute") - .setBody("{\"name\":\"new-instance\",\"description\":\"description\"}") // add remaining options + .setBody(constant("{\"name\":\"new-instance\",\"description\":\"description\"}")) // add remaining options .to("hwcloud-dms:updateInstance?instanceId=******®ion=cn-north-4&accessKey=********&secretKey=********&projectId=*******") ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-iam-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-iam-component.adoc index b6b0dc845cdd..6a160eea888d 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-iam-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-iam-component.adoc @@ -89,7 +89,7 @@ Java:: [source,java] ---- from("direct:triggerRoute") - .setBody("{\"name\":\"user\",\"description\":\"employee\",\"email\":\"[email protected]\"}") + .setBody(constant("{\"name\":\"user\",\"description\":\"employee\",\"email\":\"[email protected]\"}")) .to("hwcloud-iam:updateUser?userId=********®ion=cn-north-4&accessKey=********&secretKey=********") ---- @@ -144,7 +144,7 @@ Java:: [source,java] ---- from("direct:triggerRoute") - .setBody("{\"name\":\"group\",\"description\":\"employees\",\"domain_id\":\"1234\"}") + .setBody(constant("{\"name\":\"group\",\"description\":\"employees\",\"domain_id\":\"1234\"}")) .to("hwcloud-iam:updateUser?groupId=********®ion=cn-north-4&accessKey=********&secretKey=********") ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-obs-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-obs-component.adoc index 14b0c984a363..49f3fa4b0750 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-obs-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/hwcloud-obs-component.adoc @@ -104,7 +104,7 @@ Java:: [source,java] ---- from("direct:triggerRoute") - .setBody("{\"bucketName\":\"Bucket name\",\"location\":\"Bucket location\"}") + .setBody(constant("{\"bucketName\":\"Bucket name\",\"location\":\"Bucket location\"}")) .to("hwcloud-obs:createBucket?region=cn-north-4&accessKey=********&secretKey=********") ---- @@ -158,7 +158,7 @@ Java:: [source,java] ---- from("direct:triggerRoute") - .setBody("{\"bucketName\":\"Bucket name\",\"maxKeys\":1000}") + .setBody(constant("{\"bucketName\":\"Bucket name\",\"maxKeys\":1000}")) .to("hwcloud-obs:listObjects?region=cn-north-4&accessKey=********&secretKey=********") ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/intercept.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/intercept.adoc index 8211ded75f3c..e023a41d1ad3 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/intercept.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/intercept.adoc @@ -640,7 +640,7 @@ Java:: [source,java] ---- interceptSendToEndpoint("kafka*").skipSendToOriginalEndpoint() - .onWhen(simple("${header.biztype} == 'TEST'") + .onWhen(simple("${header.biztype} == 'TEST'")) .log("TEST message detected - is NOT send to kafka"); from("jms:queue:order") diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jdbc-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jdbc-component.adoc index 8ada1f09e9d0..d9c4efcd0e19 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jdbc-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jdbc-component.adoc @@ -93,7 +93,7 @@ Java:: from("direct:projects") .setHeader("lic", constant("ASF")) .setHeader("min", constant(123)) - .setBody("select * from projects where license = :?lic and id > :?min order by id") + .setBody(constant("select * from projects where license = :?lic and id > :?min order by id")) .to("jdbc:myDataSource?useHeadersAsParameters=true"); ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jq-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jq-language.adoc index d0199d69c8ec..b98597ad069a 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jq-language.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/jq-language.adoc @@ -27,7 +27,7 @@ To enable camel-jackson POJO type conversion, refer to the Camel Jackson documen === Using header as input -By default, JQ uses the message body as the input source. However, you can also use a header as input by specifying the `headerName` option. +By default, JQ uses the message body as the input source. However, you can also use a header, property or variable as input by specifying the `source` option, such as `header:books`. For example, to count the number of books from a JSON document that was stored in a header named `books` you can do: @@ -35,8 +35,8 @@ For example, to count the number of books from a JSON document that was stored i [source,java] ---- from("direct:start") - .setHeader("numberOfBooks") - .jq(".store.books | length", int.class, "books") + .setHeader("numberOfBooks", + expression().jq(".store.books | length").resultType(int.class).source("header:books").end()) .to("mock:result"); ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-security.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-security.adoc index 3791f75263be..9a4a7c65df64 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-security.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/keycloak-security.adoc @@ -990,7 +990,8 @@ onException(CamelAuthorizationException.class) from("rest:post:/secure-data") .doTry() .policy(keycloakPolicy) - .to("bean:dataProcessor") + .to("bean:dataProcessor") + .endDoTry() // the policy wraps the steps after it, until the end of the doTry .doCatch(CamelAuthorizationException.class) .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(403)) .transform().constant("Access denied") diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/olingo2-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/olingo2-component.adoc index a02cf1fb73e4..5fcdfdf8f378 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/olingo2-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/olingo2-component.adoc @@ -128,7 +128,7 @@ ordered by ascending Name property. [source,java] ---- from("direct:...") - .setHeader("CamelOlingo2.$top", "5"); + .setHeader("CamelOlingo2.$top", constant("5")) .to("olingo2://read/Manufacturers?orderBy=Name%20asc"); ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/olingo4-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/olingo4-component.adoc index 2792b2f1ba8a..5e125800872a 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/olingo4-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/olingo4-component.adoc @@ -118,7 +118,7 @@ ordered by ascending FirstName property. [source,java] ---- from("direct:...") - .setHeader("CamelOlingo4.$top", "5"); + .setHeader("CamelOlingo4.$top", constant("5")) .to("olingo4://read/People?orderBy=FirstName%20asc"); ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resumable-eip.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resumable-eip.adoc index 702e92e4970a..cd2c2c679afb 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resumable-eip.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resumable-eip.adoc @@ -46,7 +46,7 @@ Enable intermittent mode to avoid updating the offset for every exchange: [source,java] ---- from("some:component") - .resumable(new MyTestResumeStrategy()).intermittent(true) + .resumable().resumeStrategy(new MyTestResumeStrategy()).intermittent(true) .process(this::process); ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resume-strategies.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resume-strategies.adoc index 8bfdcdb7434d..569e0b61619e 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resume-strategies.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/resume-strategies.adoc @@ -98,7 +98,7 @@ You can enable the intermittent mode to modify the route behavior so that missin [source,java] ---- from("some:component") -.resumable(new MyTestResumeStrategy()).intermittent(true) +.resumable().resumeStrategy(new MyTestResumeStrategy()).intermittent(true) .process(this::process) ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/seda-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/seda-component.adoc index b9b9befc43ec..53b298cfe7fc 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/seda-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/seda-component.adoc @@ -199,7 +199,7 @@ like: ._Java-only: thread pool added to SEDA endpoint_ [source,java] ---- -from("seda:stageName").thread(5).process(...) +from("seda:stageName").threads(5).process(...) ---- Can wind up with two `BlockQueues`: one from the SEDA endpoint, and one @@ -211,7 +211,7 @@ asynchronously. For example: ._Java-only: thread pool on a Direct endpoint_ [source,java] ---- -from("direct:stageName").thread(5).process(...) +from("direct:stageName").threads(5).process(...) ---- You can also directly configure number of threads that process messages diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/validate-eip.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/validate-eip.adoc index 06fd3bfa3c87..e694bf9c56de 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/validate-eip.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/validate-eip.adoc @@ -38,7 +38,7 @@ Java:: [source,java] ---- from("file:inbox") - .validate(body(String.class).regex("^\\w{10}\\,\\d{2}\\,\\w{24}$")) + .validate(bodyAs(String.class).regex("^\\w{10}\\,\\d{2}\\,\\w{24}$")) .to("bean:myServiceBean.processLine"); ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xmlSecurity-dataformat.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xmlSecurity-dataformat.adoc index ced411c85754..e7b53ee5d873 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xmlSecurity-dataformat.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/xmlSecurity-dataformat.adoc @@ -89,7 +89,7 @@ Key key = keyGenerator.generateKey(); from("direct:start") .marshal().xmlSecurity(key.getEncoded()) - .unmarshal().xmlSecurity(key.getEncoded() + .unmarshal().xmlSecurity(key.getEncoded()) .to("direct:end"); ---- @@ -160,7 +160,7 @@ Java:: [source,java] ---- from("direct:start") - .marshal().xmlSecurity("//cheese:cheesesites/italy", true) + .marshal().xmlSecurity("//cheese:cheesesites/italy", true, "Just another 32 Byte key for AES") .to("..."); ---- @@ -175,7 +175,8 @@ XML:: <from uri="direct://start"/> <marshal> <xmlSecurity secureTag="//cheese:cheesesites/italy" - secureTagContents="true"/> + secureTagContents="true" + passPhrase="Just another 32 Byte key for AES"/> </marshal> ... ---- @@ -192,6 +193,7 @@ YAML:: xmlSecurity: secureTag: "//cheese:cheesesites/italy" secureTagContents: true + passPhrase: "Just another 32 Byte key for AES" ---- ==== diff --git a/components/camel-aws/camel-aws2-ses/src/main/docs/aws2-ses-component.adoc b/components/camel-aws/camel-aws2-ses/src/main/docs/aws2-ses-component.adoc index 7ab6c2dbe639..4368831ee4cb 100644 --- a/components/camel-aws/camel-aws2-ses/src/main/docs/aws2-ses-component.adoc +++ b/components/camel-aws/camel-aws2-ses/src/main/docs/aws2-ses-component.adoc @@ -121,7 +121,7 @@ Java:: ---- from("direct:start") .setHeader(SesConstants.SUBJECT, constant("This is my subject")) - .setHeader(SesConstants.TO, constant(Collections.singletonList("[email protected]")) + .setHeader(SesConstants.TO, constant(Collections.singletonList("[email protected]"))) .setBody(constant("This is my message text.")) .to("aws2-ses://[email protected]?accessKey=xxx&secretKey=yyy"); ---- diff --git a/components/camel-aws/camel-aws2-sqs/src/main/docs/aws2-sqs-component.adoc b/components/camel-aws/camel-aws2-sqs/src/main/docs/aws2-sqs-component.adoc index 89fa5105095b..0b355120b900 100644 --- a/components/camel-aws/camel-aws2-sqs/src/main/docs/aws2-sqs-component.adoc +++ b/components/camel-aws/camel-aws2-sqs/src/main/docs/aws2-sqs-component.adoc @@ -138,7 +138,7 @@ Java:: [source,java] ---- from("aws2-sqs://MyQueue?amazonSQSClient=#client&defaultVisibilityTimeout=5000&deleteIfFiltered=false&deleteAfterRead=false") -.filter("${header.login} == true") +.filter(simple("${header.login} == true")) .setProperty("CamelAwsSqsDeleteFiltered", constant(true)) .to("mock:filter"); ---- diff --git a/components/camel-aws/camel-aws2-translate/src/main/docs/aws2-translate-component.adoc b/components/camel-aws/camel-aws2-translate/src/main/docs/aws2-translate-component.adoc index 8c976e072bc7..bb6cf7ae4179 100644 --- a/components/camel-aws/camel-aws2-translate/src/main/docs/aws2-translate-component.adoc +++ b/components/camel-aws/camel-aws2-translate/src/main/docs/aws2-translate-component.adoc @@ -84,7 +84,7 @@ Camel-AWS Translate component provides the following operation on the producer s from("direct:start") .setHeader(TranslateConstants.SOURCE_LANGUAGE, TranslateLanguageEnum.ITALIAN) .setHeader(TranslateConstants.TARGET_LANGUAGE, TranslateLanguageEnum.GERMAN) - .setBody("Ciao") + .setBody(constant("Ciao")) .to("aws2-translate://test?translateClient=#amazonTranslateClient&operation=translateText"); ---- diff --git a/components/camel-disruptor/src/main/docs/disruptor-component.adoc b/components/camel-disruptor/src/main/docs/disruptor-component.adoc index a864a33aff77..4fb1d1e7c904 100644 --- a/components/camel-disruptor/src/main/docs/disruptor-component.adoc +++ b/components/camel-disruptor/src/main/docs/disruptor-component.adoc @@ -196,7 +196,7 @@ something like: ._Java-only: incomplete route (thread pool example)_ [source,java] ---- -from("disruptor:stageName").thread(5).process(...) +from("disruptor:stageName").threads(5).process(...) ---- Can wind up with adding a normal diff --git a/components/camel-google/camel-google-bigquery/src/main/docs/google-bigquery-component.adoc b/components/camel-google/camel-google-bigquery/src/main/docs/google-bigquery-component.adoc index 6685537b880a..965789ea0996 100644 --- a/components/camel-google/camel-google-bigquery/src/main/docs/google-bigquery-component.adoc +++ b/components/camel-google/camel-google-bigquery/src/main/docs/google-bigquery-component.adoc @@ -109,7 +109,7 @@ I.e. the following route will create tables and insert records sharded on a per- [source,java] ---- from("direct:start") - .header(GoogleBigQueryConstants.TABLE_SUFFIX, "_${date:now:yyyyMMdd}") + .setHeader(GoogleBigQueryConstants.TABLE_SUFFIX, simple("_${date:now:yyyyMMdd}")) .to("google-bigquery:sampleDataset:sampleTable") ---- Note it is recommended to use partitioning for this use case. diff --git a/components/camel-huawei/camel-huaweicloud-dms/src/main/docs/hwcloud-dms-component.adoc b/components/camel-huawei/camel-huaweicloud-dms/src/main/docs/hwcloud-dms-component.adoc index f747c4dabadf..e1264510d92a 100644 --- a/components/camel-huawei/camel-huaweicloud-dms/src/main/docs/hwcloud-dms-component.adoc +++ b/components/camel-huawei/camel-huaweicloud-dms/src/main/docs/hwcloud-dms-component.adoc @@ -119,7 +119,7 @@ To create an instance, you can pass the parameters through the endpoint, the exc [source,java] ---- from("direct:triggerRoute") - .setBody(new CreateInstanceRequestBody().withName("new-instance").withDescription("description").with*) // add remaining options + .setBody(constant(new CreateInstanceRequestBody().withName("new-instance").withDescription("description").with*)) // add remaining options .to("hwcloud-dms:createInstance?region=cn-north-4&accessKey=********&secretKey=********&projectId=*******") ---- @@ -130,7 +130,7 @@ Java:: [source,java] ---- from("direct:triggerRoute") - .setBody("{\"name\":\"new-instance\",\"description\":\"description\"}") // add remaining options + .setBody(constant("{\"name\":\"new-instance\",\"description\":\"description\"}")) // add remaining options .to("hwcloud-dms:createInstance?region=cn-north-4&accessKey=********&secretKey=********&projectId=*******") ---- @@ -175,7 +175,7 @@ To update an instance, you must pass the parameters through the exchange body as [source,java] ---- from("direct:triggerRoute") - .setBody(new UpdateInstanceRequestBody().withName("new-instance").withDescription("description").with*) // add remaining options + .setBody(constant(new UpdateInstanceRequestBody().withName("new-instance").withDescription("description").with*)) // add remaining options .to("hwcloud-dms:updateInstance?instanceId=******®ion=cn-north-4&accessKey=********&secretKey=********&projectId=*******") ---- @@ -186,7 +186,7 @@ Java:: [source,java] ---- from("direct:triggerRoute") - .setBody("{\"name\":\"new-instance\",\"description\":\"description\"}") // add remaining options + .setBody(constant("{\"name\":\"new-instance\",\"description\":\"description\"}")) // add remaining options .to("hwcloud-dms:updateInstance?instanceId=******®ion=cn-north-4&accessKey=********&secretKey=********&projectId=*******") ---- diff --git a/components/camel-huawei/camel-huaweicloud-iam/src/main/docs/hwcloud-iam-component.adoc b/components/camel-huawei/camel-huaweicloud-iam/src/main/docs/hwcloud-iam-component.adoc index b6b0dc845cdd..6a160eea888d 100644 --- a/components/camel-huawei/camel-huaweicloud-iam/src/main/docs/hwcloud-iam-component.adoc +++ b/components/camel-huawei/camel-huaweicloud-iam/src/main/docs/hwcloud-iam-component.adoc @@ -89,7 +89,7 @@ Java:: [source,java] ---- from("direct:triggerRoute") - .setBody("{\"name\":\"user\",\"description\":\"employee\",\"email\":\"[email protected]\"}") + .setBody(constant("{\"name\":\"user\",\"description\":\"employee\",\"email\":\"[email protected]\"}")) .to("hwcloud-iam:updateUser?userId=********®ion=cn-north-4&accessKey=********&secretKey=********") ---- @@ -144,7 +144,7 @@ Java:: [source,java] ---- from("direct:triggerRoute") - .setBody("{\"name\":\"group\",\"description\":\"employees\",\"domain_id\":\"1234\"}") + .setBody(constant("{\"name\":\"group\",\"description\":\"employees\",\"domain_id\":\"1234\"}")) .to("hwcloud-iam:updateUser?groupId=********®ion=cn-north-4&accessKey=********&secretKey=********") ---- diff --git a/components/camel-huawei/camel-huaweicloud-obs/src/main/docs/hwcloud-obs-component.adoc b/components/camel-huawei/camel-huaweicloud-obs/src/main/docs/hwcloud-obs-component.adoc index 14b0c984a363..49f3fa4b0750 100644 --- a/components/camel-huawei/camel-huaweicloud-obs/src/main/docs/hwcloud-obs-component.adoc +++ b/components/camel-huawei/camel-huaweicloud-obs/src/main/docs/hwcloud-obs-component.adoc @@ -104,7 +104,7 @@ Java:: [source,java] ---- from("direct:triggerRoute") - .setBody("{\"bucketName\":\"Bucket name\",\"location\":\"Bucket location\"}") + .setBody(constant("{\"bucketName\":\"Bucket name\",\"location\":\"Bucket location\"}")) .to("hwcloud-obs:createBucket?region=cn-north-4&accessKey=********&secretKey=********") ---- @@ -158,7 +158,7 @@ Java:: [source,java] ---- from("direct:triggerRoute") - .setBody("{\"bucketName\":\"Bucket name\",\"maxKeys\":1000}") + .setBody(constant("{\"bucketName\":\"Bucket name\",\"maxKeys\":1000}")) .to("hwcloud-obs:listObjects?region=cn-north-4&accessKey=********&secretKey=********") ---- diff --git a/components/camel-jdbc/src/main/docs/jdbc-component.adoc b/components/camel-jdbc/src/main/docs/jdbc-component.adoc index 8ada1f09e9d0..d9c4efcd0e19 100644 --- a/components/camel-jdbc/src/main/docs/jdbc-component.adoc +++ b/components/camel-jdbc/src/main/docs/jdbc-component.adoc @@ -93,7 +93,7 @@ Java:: from("direct:projects") .setHeader("lic", constant("ASF")) .setHeader("min", constant(123)) - .setBody("select * from projects where license = :?lic and id > :?min order by id") + .setBody(constant("select * from projects where license = :?lic and id > :?min order by id")) .to("jdbc:myDataSource?useHeadersAsParameters=true"); ---- diff --git a/components/camel-jq/src/main/docs/jq-language.adoc b/components/camel-jq/src/main/docs/jq-language.adoc index d0199d69c8ec..b98597ad069a 100644 --- a/components/camel-jq/src/main/docs/jq-language.adoc +++ b/components/camel-jq/src/main/docs/jq-language.adoc @@ -27,7 +27,7 @@ To enable camel-jackson POJO type conversion, refer to the Camel Jackson documen === Using header as input -By default, JQ uses the message body as the input source. However, you can also use a header as input by specifying the `headerName` option. +By default, JQ uses the message body as the input source. However, you can also use a header, property or variable as input by specifying the `source` option, such as `header:books`. For example, to count the number of books from a JSON document that was stored in a header named `books` you can do: @@ -35,8 +35,8 @@ For example, to count the number of books from a JSON document that was stored i [source,java] ---- from("direct:start") - .setHeader("numberOfBooks") - .jq(".store.books | length", int.class, "books") + .setHeader("numberOfBooks", + expression().jq(".store.books | length").resultType(int.class).source("header:books").end()) .to("mock:result"); ---- diff --git a/components/camel-keycloak/src/main/docs/keycloak-security.adoc b/components/camel-keycloak/src/main/docs/keycloak-security.adoc index 3791f75263be..9a4a7c65df64 100644 --- a/components/camel-keycloak/src/main/docs/keycloak-security.adoc +++ b/components/camel-keycloak/src/main/docs/keycloak-security.adoc @@ -990,7 +990,8 @@ onException(CamelAuthorizationException.class) from("rest:post:/secure-data") .doTry() .policy(keycloakPolicy) - .to("bean:dataProcessor") + .to("bean:dataProcessor") + .endDoTry() // the policy wraps the steps after it, until the end of the doTry .doCatch(CamelAuthorizationException.class) .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(403)) .transform().constant("Access denied") diff --git a/components/camel-olingo2/camel-olingo2-component/src/main/docs/olingo2-component.adoc b/components/camel-olingo2/camel-olingo2-component/src/main/docs/olingo2-component.adoc index a02cf1fb73e4..5fcdfdf8f378 100644 --- a/components/camel-olingo2/camel-olingo2-component/src/main/docs/olingo2-component.adoc +++ b/components/camel-olingo2/camel-olingo2-component/src/main/docs/olingo2-component.adoc @@ -128,7 +128,7 @@ ordered by ascending Name property. [source,java] ---- from("direct:...") - .setHeader("CamelOlingo2.$top", "5"); + .setHeader("CamelOlingo2.$top", constant("5")) .to("olingo2://read/Manufacturers?orderBy=Name%20asc"); ---- diff --git a/components/camel-olingo4/camel-olingo4-component/src/main/docs/olingo4-component.adoc b/components/camel-olingo4/camel-olingo4-component/src/main/docs/olingo4-component.adoc index 2792b2f1ba8a..5e125800872a 100644 --- a/components/camel-olingo4/camel-olingo4-component/src/main/docs/olingo4-component.adoc +++ b/components/camel-olingo4/camel-olingo4-component/src/main/docs/olingo4-component.adoc @@ -118,7 +118,7 @@ ordered by ascending FirstName property. [source,java] ---- from("direct:...") - .setHeader("CamelOlingo4.$top", "5"); + .setHeader("CamelOlingo4.$top", constant("5")) .to("olingo4://read/People?orderBy=FirstName%20asc"); ---- diff --git a/components/camel-seda/src/main/docs/seda-component.adoc b/components/camel-seda/src/main/docs/seda-component.adoc index b9b9befc43ec..53b298cfe7fc 100644 --- a/components/camel-seda/src/main/docs/seda-component.adoc +++ b/components/camel-seda/src/main/docs/seda-component.adoc @@ -199,7 +199,7 @@ like: ._Java-only: thread pool added to SEDA endpoint_ [source,java] ---- -from("seda:stageName").thread(5).process(...) +from("seda:stageName").threads(5).process(...) ---- Can wind up with two `BlockQueues`: one from the SEDA endpoint, and one @@ -211,7 +211,7 @@ asynchronously. For example: ._Java-only: thread pool on a Direct endpoint_ [source,java] ---- -from("direct:stageName").thread(5).process(...) +from("direct:stageName").threads(5).process(...) ---- You can also directly configure number of threads that process messages diff --git a/components/camel-snakeyaml/src/test/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormatClauseTest.java b/components/camel-snakeyaml/src/test/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormatClauseTest.java new file mode 100644 index 000000000000..e841a6c8f1c5 --- /dev/null +++ b/components/camel-snakeyaml/src/test/java/org/apache/camel/component/snakeyaml/SnakeYAMLDataFormatClauseTest.java @@ -0,0 +1,49 @@ +/* + * 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.snakeyaml; + +import java.util.Map; + +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * The YAML data format from the data format clause: {@code .marshal().yaml()}, as the documentation shows. + */ +public class SnakeYAMLDataFormatClauseTest extends CamelTestSupport { + + @Test + public void marshalAndUnmarshal() { + String yaml = template.requestBody("direct:marshal", Map.of("name", "Camel"), String.class); + assertEquals("{name: Camel}", yaml.trim()); + assertEquals(Map.of("name", "Camel"), template.requestBody("direct:unmarshal", yaml, Map.class)); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:marshal").marshal().yaml(); + from("direct:unmarshal").unmarshal().yaml(); + } + }; + } +} diff --git a/components/camel-univocity-parsers/src/test/java/org/apache/camel/dataformat/univocity/UniVocityDataFormatClauseTest.java b/components/camel-univocity-parsers/src/test/java/org/apache/camel/dataformat/univocity/UniVocityDataFormatClauseTest.java new file mode 100644 index 000000000000..c0a665efadcf --- /dev/null +++ b/components/camel-univocity-parsers/src/test/java/org/apache/camel/dataformat/univocity/UniVocityDataFormatClauseTest.java @@ -0,0 +1,63 @@ +/* + * 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.dataformat.univocity; + +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.model.MarshalDefinition; +import org.apache.camel.model.dataformat.UniVocityFixedDataFormat; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; + +import static org.apache.camel.dataformat.univocity.UniVocityTestHelper.asMap; +import static org.apache.camel.dataformat.univocity.UniVocityTestHelper.join; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +/** + * The uniVocity data formats from the data format clause: {@code .marshal().univocityCsv()}, as the documentation + * shows. + */ +public class UniVocityDataFormatClauseTest extends CamelTestSupport { + + @Test + public void marshalCsv() { + assertEquals(join("1,2,3"), template.requestBody("direct:csv", asMap("A", "1", "B", "2", "C", "3"), String.class)); + } + + @Test + public void marshalTsv() { + assertEquals(join("1\t2\t3"), template.requestBody("direct:tsv", asMap("A", "1", "B", "2", "C", "3"), String.class)); + } + + @Test + public void fixedInTheModel() { + MarshalDefinition marshal = (MarshalDefinition) context.getRouteDefinition("fixed").getOutputs().get(0); + assertInstanceOf(UniVocityFixedDataFormat.class, marshal.getDataFormatType()); + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + from("direct:csv").marshal().univocityCsv(); + from("direct:tsv").marshal().univocityTsv(); + from("direct:fixed").routeId("fixed").marshal().univocityFixed(); + } + }; + } +} diff --git a/components/camel-xmlsecurity/src/main/docs/xmlSecurity-dataformat.adoc b/components/camel-xmlsecurity/src/main/docs/xmlSecurity-dataformat.adoc index ced411c85754..e7b53ee5d873 100644 --- a/components/camel-xmlsecurity/src/main/docs/xmlSecurity-dataformat.adoc +++ b/components/camel-xmlsecurity/src/main/docs/xmlSecurity-dataformat.adoc @@ -89,7 +89,7 @@ Key key = keyGenerator.generateKey(); from("direct:start") .marshal().xmlSecurity(key.getEncoded()) - .unmarshal().xmlSecurity(key.getEncoded() + .unmarshal().xmlSecurity(key.getEncoded()) .to("direct:end"); ---- @@ -160,7 +160,7 @@ Java:: [source,java] ---- from("direct:start") - .marshal().xmlSecurity("//cheese:cheesesites/italy", true) + .marshal().xmlSecurity("//cheese:cheesesites/italy", true, "Just another 32 Byte key for AES") .to("..."); ---- @@ -175,7 +175,8 @@ XML:: <from uri="direct://start"/> <marshal> <xmlSecurity secureTag="//cheese:cheesesites/italy" - secureTagContents="true"/> + secureTagContents="true" + passPhrase="Just another 32 Byte key for AES"/> </marshal> ... ---- @@ -192,6 +193,7 @@ YAML:: xmlSecurity: secureTag: "//cheese:cheesesites/italy" secureTagContents: true + passPhrase: "Just another 32 Byte key for AES" ---- ==== diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/dead-letter-channel.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/dead-letter-channel.adoc index 93e755f663a7..2df230324a93 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/dead-letter-channel.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/dead-letter-channel.adoc @@ -324,7 +324,7 @@ Java:: ---- errorHandler(deadLetterChannel("jms:queue:dead") .maximumRedeliveries(3) - .onRedeliver(new MyOnRedeliveryProcessor()); + .onRedelivery(new MyOnRedeliveryProcessor())); ---- Spring XML:: diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/intercept.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/intercept.adoc index 8211ded75f3c..e023a41d1ad3 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/intercept.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/intercept.adoc @@ -640,7 +640,7 @@ Java:: [source,java] ---- interceptSendToEndpoint("kafka*").skipSendToOriginalEndpoint() - .onWhen(simple("${header.biztype} == 'TEST'") + .onWhen(simple("${header.biztype} == 'TEST'")) .log("TEST message detected - is NOT send to kafka"); from("jms:queue:order") diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/resumable-eip.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/resumable-eip.adoc index 702e92e4970a..cd2c2c679afb 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/resumable-eip.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/resumable-eip.adoc @@ -46,7 +46,7 @@ Enable intermittent mode to avoid updating the offset for every exchange: [source,java] ---- from("some:component") - .resumable(new MyTestResumeStrategy()).intermittent(true) + .resumable().resumeStrategy(new MyTestResumeStrategy()).intermittent(true) .process(this::process); ---- diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/resume-strategies.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/resume-strategies.adoc index 8bfdcdb7434d..569e0b61619e 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/resume-strategies.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/resume-strategies.adoc @@ -98,7 +98,7 @@ You can enable the intermittent mode to modify the route behavior so that missin [source,java] ---- from("some:component") -.resumable(new MyTestResumeStrategy()).intermittent(true) +.resumable().resumeStrategy(new MyTestResumeStrategy()).intermittent(true) .process(this::process) ---- diff --git a/core/camel-core-engine/src/main/docs/modules/eips/pages/validate-eip.adoc b/core/camel-core-engine/src/main/docs/modules/eips/pages/validate-eip.adoc index 06fd3bfa3c87..e694bf9c56de 100644 --- a/core/camel-core-engine/src/main/docs/modules/eips/pages/validate-eip.adoc +++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/validate-eip.adoc @@ -38,7 +38,7 @@ Java:: [source,java] ---- from("file:inbox") - .validate(body(String.class).regex("^\\w{10}\\,\\d{2}\\,\\w{24}$")) + .validate(bodyAs(String.class).regex("^\\w{10}\\,\\d{2}\\,\\w{24}$")) .to("bean:myServiceBean.processLine"); ---- diff --git a/core/camel-core-model/src/main/java/org/apache/camel/builder/DataFormatClause.java b/core/camel-core-model/src/main/java/org/apache/camel/builder/DataFormatClause.java index a1549752cd10..8fc664634ad7 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/builder/DataFormatClause.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/builder/DataFormatClause.java @@ -66,6 +66,9 @@ import org.apache.camel.model.dataformat.TarFileDataFormat; import org.apache.camel.model.dataformat.ThriftDataFormat; import org.apache.camel.model.dataformat.ToonDataFormat; import org.apache.camel.model.dataformat.UblDataFormat; +import org.apache.camel.model.dataformat.UniVocityCsvDataFormat; +import org.apache.camel.model.dataformat.UniVocityFixedDataFormat; +import org.apache.camel.model.dataformat.UniVocityTsvDataFormat; import org.apache.camel.model.dataformat.XMLSecurityDataFormat; import org.apache.camel.model.dataformat.YAMLDataFormat; import org.apache.camel.model.dataformat.YAMLLibrary; @@ -1121,6 +1124,34 @@ public class DataFormatClause<T extends ProcessorDefinition<?>> { return dataFormat(ublDataFormat); } + /** + * Uses the uniVocity CSV data format + */ + public T univocityCsv() { + return dataFormat(new UniVocityCsvDataFormat()); + } + + /** + * Uses the uniVocity fixed-length data format + */ + public T univocityFixed() { + return dataFormat(new UniVocityFixedDataFormat()); + } + + /** + * Uses the uniVocity TSV data format + */ + public T univocityTsv() { + return dataFormat(new UniVocityTsvDataFormat()); + } + + /** + * Uses the YAML data format with the SnakeYAML library + */ + public T yaml() { + return dataFormat(new YAMLDataFormat()); + } + /** * Uses the YAML data format * diff --git a/core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java b/core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java index 6276d223d68b..59e7272e6623 100644 --- a/core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java +++ b/core/camel-java-io/src/main/java/org/apache/camel/java/in/ChainReplayer.java @@ -32,11 +32,13 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.IllegalFormatException; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -51,6 +53,7 @@ import org.apache.camel.java.in.JavaChainParser.BinOp; import org.apache.camel.java.in.JavaChainParser.Bool; import org.apache.camel.java.in.JavaChainParser.Call; import org.apache.camel.java.in.JavaChainParser.Chain; +import org.apache.camel.java.in.JavaChainParser.Chr; import org.apache.camel.java.in.JavaChainParser.ClassLit; import org.apache.camel.java.in.JavaChainParser.ClassName; import org.apache.camel.java.in.JavaChainParser.Concat; @@ -112,7 +115,7 @@ final class ChainReplayer { private static final Set<String> READABLE_JDK = Set.of( "java.lang.Integer", "java.lang.Long", "java.lang.Short", "java.lang.Byte", "java.lang.Double", "java.lang.Float", "java.lang.Boolean", "java.lang.Character", "java.util.concurrent.TimeUnit", - "java.nio.charset.StandardCharsets"); + "java.nio.charset.StandardCharsets", "java.util.zip.Deflater"); /** Classes whose static DSL methods a route may call, qualified or statically imported: language(...), and(...). */ private static final List<Class<?>> STATIC_DSL @@ -129,12 +132,13 @@ final class ChainReplayer { private static final Set<String> DENIED_METHODS = Set.of( "getContext", "getCamelContext", "setContext", "setCamelContext", "addRoutesToCamelContext", - "addRouteConfigurationsToCamelContext", "addTemplatedRoutesToCamelContext", "configure", "configuration", + "addRouteConfigurationsToCamelContext", "addTemplatedRoutesToCamelContext", "includeRoutes", "bindToRegistry", "propertyInject", "endpoint", "getClass", "wait", "notify", "notifyAll"); /** The life cycle of a route builder, which a parse never runs: only the DSL it offers is called. */ private static final List<String> BUILDER_LIFE_CYCLE = List.of( - "populate", "configure", "prepare", "update", "initialize", "set", "check", "add", "remove", "customize"); + "populate", "configure", "configuration", "prepare", "update", "initialize", "set", "check", "add", "remove", + "customize"); /** The builder of the configure() being replayed. */ private ReplayBuilder builder; @@ -241,6 +245,8 @@ final class ChainReplayer { private Object evaluate(Node node) { if (node instanceof Str s) { return s.value(); + } else if (node instanceof Chr c) { + return c.value(); } else if (node instanceof Num n) { return number(n.text()); } else if (node instanceof Bool b) { @@ -540,6 +546,12 @@ final class ChainReplayer { for (String pkg : CAMEL_PACKAGES) { candidates.add(pkg + name); } + // a snippet without its imports: TimeUnit.SECONDS, Deflater.BEST_COMPRESSION + for (String jdk : READABLE_JDK) { + if (jdk.endsWith("." + first)) { + candidates.add(jdk + name.substring(first.length())); + } + } } for (String candidate : candidates) { String binary = candidate; @@ -604,6 +616,11 @@ final class ChainReplayer { String text = formatText(c.calls().get(0).args()); return text != null ? text : new Unknown(c, "a format the parser cannot work out"); } + if (c.qualifier() != null && c.calls().size() == 1 + && COLLECTIONS.contains(c.qualifier() + "." + c.calls().get(0).name())) { + // setHeaders(Map.of("foo", constant("ABC"))): a collection of values the parser works out + return collection(c); + } if (c.qualifier() != null && c.qualifier().equals(builderParameter)) { // rb.simple(...) in a builder lambda: the builder target = builder; @@ -662,6 +679,36 @@ final class ChainReplayer { return target; } + /** Factories of JDK collections a route passes values in: only data, nothing of the project runs. */ + private static final Set<String> COLLECTIONS = Set.of( + "Map.of", "java.util.Map.of", "List.of", "java.util.List.of", "Set.of", "java.util.Set.of", + "Arrays.asList", "java.util.Arrays.asList", "Collections.singletonList", "java.util.Collections.singletonList"); + + private Object collection(Chain c) { + Call call = c.calls().get(0); + List<Object> values = new ArrayList<>(); + for (Node arg : call.args()) { + Object v = evaluate(arg); + if (v instanceof Unknown || v == null) { + return new Unknown(c, "a collection of values the parser cannot work out"); + } + values.add(v); + } + String qualifier = c.qualifier().substring(c.qualifier().lastIndexOf('.') + 1); + if (qualifier.equals("Map")) { + if (values.size() % 2 != 0) { + return new Unknown(c, "a collection of values the parser cannot work out"); + } + // in the order of the source, which Map.of does not keep but the model and a dump should + Map<Object, Object> map = new LinkedHashMap<>(); + for (int i = 0; i < values.size(); i += 2) { + map.put(values.get(i), values.get(i + 1)); + } + return map; + } + return qualifier.equals("Set") ? new LinkedHashSet<>(values) : values; + } + /** Why a statement that sets up the CamelContext (components, beans, properties) is not read: it is not a route. */ static final String CONFIGURES_THE_CONTEXT = "configures the CamelContext, not a route"; @@ -788,9 +835,21 @@ final class ChainReplayer { /** The URI of an endpoint DSL chain, or null when the resolver does not know its factory. */ private String endpoint(Chain c) { Call factory = c.calls().get(0); - List<String> args = new ArrayList<>(); + if (factory.args().size() > 2) { + return null; + } + List<Object> paths = new ArrayList<>(); for (Node arg : factory.args()) { - args.add(uriValue(arg)); + Object v = evaluate(arg); + if (!(v instanceof String) && !(v instanceof Unknown)) { + // a factory of the endpoint DSL takes the path as text: split(stax(Record.class)) is StAXBuilder + return null; + } + paths.add(v); + } + List<String> args = new ArrayList<>(); + for (int i = 0; i < paths.size(); i++) { + args.add(uriValue(factory.args().get(i), paths.get(i))); } List<EndpointDslResolver.Option> options = new ArrayList<>(); for (int i = 1; i < c.calls().size(); i++) { @@ -813,7 +872,10 @@ final class ChainReplayer { /** A value in an endpoint URI as the endpoint DSL writes it; a marked placeholder when unknown (reported). */ private String uriValue(Node node) { - Object v = evaluate(node); + return uriValue(node, evaluate(node)); + } + + private String uriValue(Node node, Object v) { if (v instanceof Unknown u) { report(node, u.reason()); return LwJavaParser.UNRESOLVED_PREFIX + text(node) + "}"; @@ -845,6 +907,10 @@ final class ChainReplayer { } private Object invoke(Object target, Call call) { + Object value = valueCall(target, call); + if (value != null) { + return value; + } List<Method> candidates = candidates(target.getClass(), call.name(), call.args().size()); if (candidates.isEmpty()) { String reason = target == builder @@ -855,6 +921,31 @@ final class ChainReplayer { return call(target, candidates, call); } + /** + * A call on a JDK value that only computes a value: {@code TimeUnit.MILLISECONDS.toString()}, + * {@code TimeUnit.SECONDS.toMillis(5)}; null for anything else. + */ + private Object valueCall(Object target, Call call) { + if (target instanceof Enum<?> e && call.args().isEmpty() + && (call.name().equals("name") || call.name().equals("toString"))) { + return e.name(); + } + if (target instanceof TimeUnit unit && call.args().size() == 1 && call.name().startsWith("to") + && evaluate(call.args().get(0)) instanceof Number n) { + return switch (call.name()) { + case "toNanos" -> unit.toNanos(n.longValue()); + case "toMicros" -> unit.toMicros(n.longValue()); + case "toMillis" -> unit.toMillis(n.longValue()); + case "toSeconds" -> unit.toSeconds(n.longValue()); + case "toMinutes" -> unit.toMinutes(n.longValue()); + case "toHours" -> unit.toHours(n.longValue()); + case "toDays" -> unit.toDays(n.longValue()); + default -> null; + }; + } + return null; + } + /** Calls the overload the arguments fit best; a static one when {@code target} is null. */ private Object call(Object target, List<Method> candidates, Call call) { Object[] values = new Object[call.args().size()]; @@ -905,6 +996,11 @@ final class ChainReplayer { } catch (InvocationTargetException e) { Throwable cause = e.getCause() != null ? e.getCause() : e; String message = String.valueOf(cause.getMessage()); + if (standsIn(values)) { + // the DSL checks or uses right away what stands in for a value: configuration(builder) calls + // builder.build(), setHeaders(headerMap) needs the map; the source is not wrong + return new Unknown(call, "needs a value the parser cannot see"); + } if (cause instanceof NullPointerException && message.contains("CamelContext")) { // the replay builder never has a context: whatever needs one is not done in a parse return new Unknown(call, "needs a running CamelContext, which a parse does not have"); @@ -915,6 +1011,16 @@ final class ChainReplayer { } } + /** Whether something stands in for a value the parser does not know, in the arguments of a call. */ + private static boolean standsIn(Object[] values) { + for (Object v : values) { + if (v instanceof Unknown) { + return true; + } + } + return false; + } + /** How well a method's parameter types match the values: exactly the value's type counts most, Object least. */ private static int specificity(Method m, Object[] values) { Class<?>[] types = m.getParameterTypes(); @@ -1143,6 +1249,13 @@ final class ChainReplayer { default -> defaultValue(method.getReturnType()); }); } + if (!type.isPrimitive() && !type.isEnum() && type != Class.class) { + // an object of a class, such as xpath("/c:n", ns) with Namespaces ns built in the method: null stands + // in, as the parser creates no objects of a route + cost[0] += 30; + reports.add(new JavaParseResult.Unresolved(node.line(), text, u.reason())); + return null; + } return NO; } @@ -1199,6 +1312,8 @@ final class ChainReplayer { static String text(Node node) { if (node instanceof Str s) { return "\"" + s.value() + "\""; + } else if (node instanceof Chr c) { + return "'" + c.value() + "'"; } else if (node instanceof Num n) { return n.text(); } else if (node instanceof Bool b) { diff --git a/core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaChainParser.java b/core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaChainParser.java index 8b37fc25cdb9..fa4381fcb792 100644 --- a/core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaChainParser.java +++ b/core/camel-java-io/src/main/java/org/apache/camel/java/in/JavaChainParser.java @@ -45,6 +45,10 @@ final class JavaChainParser { record Num(String text, int line) implements Node { } + /** A char literal: {@code 'i'}. */ + record Chr(char value, int line) implements Node { + } + record Bool(boolean value, int line) implements Node { } @@ -428,10 +432,14 @@ final class JavaChainParser { Token t = peek(); int line = t.line(); switch (t.kind()) { - case STRING, CHAR -> { + case STRING -> { pos++; return new Str(t.text(), line); } + case CHAR -> { + pos++; + return t.text().length() == 1 ? new Chr(t.text().charAt(0), line) : new Str(t.text(), line); + } case NUMBER -> { pos++; return new Num(t.text(), line); diff --git a/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java b/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java index 79169e1784ee..9b8fe57aa64a 100644 --- a/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java +++ b/core/camel-java-io/src/test/java/org/apache/camel/java/in/LwJavaParserTest.java @@ -19,13 +19,19 @@ package org.apache.camel.java.in; import java.util.List; import org.apache.camel.model.ChoiceDefinition; +import org.apache.camel.model.CircuitBreakerDefinition; +import org.apache.camel.model.DelayDefinition; +import org.apache.camel.model.FilterDefinition; +import org.apache.camel.model.MarshalDefinition; import org.apache.camel.model.OnExceptionDefinition; import org.apache.camel.model.ProcessDefinition; import org.apache.camel.model.RouteDefinition; import org.apache.camel.model.SetHeaderDefinition; +import org.apache.camel.model.SetHeadersDefinition; import org.apache.camel.model.SplitDefinition; import org.apache.camel.model.ToDefinition; import org.apache.camel.model.TryDefinition; +import org.apache.camel.model.dataformat.ZipDeflaterDataFormat; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -143,6 +149,66 @@ class LwJavaParserTest { assertThat(result.isComplete()).as("%s", result.unresolved()).isTrue(); } + @Test + void charLiteralsCollectionsAndJdkConstants() { + // CAMEL-25182: shapes of the Java examples of the documentation + JavaParseResult result = new LwJavaParser().parse(""" + from("direct:a") + .split(body().tokenize(",")) + .setHeaders(Map.of("foo", constant("ABC"), "bar", "XYZ")) + .marshal().zipDeflater(Deflater.BEST_COMPRESSION) + .setHeader("unit").constant(TimeUnit.MILLISECONDS.toString()) + .delay(TimeUnit.SECONDS.toMillis(5)) + .to("mock:a"); + """); + assertThat(result.isComplete()).as("%s", result.unresolved()).isTrue(); + RouteDefinition route = result.routes().getRoutes().get(0); + SplitDefinition split = (SplitDefinition) route.getOutputs().get(0); + SetHeadersDefinition headers = (SetHeadersDefinition) split.getOutputs().get(0); + assertThat(headers.getHeaders()).extracting(SetHeaderDefinition::getName).containsExactly("foo", "bar"); + MarshalDefinition marshal = (MarshalDefinition) split.getOutputs().get(1); + assertThat(((ZipDeflaterDataFormat) marshal.getDataFormatType()).getCompressionLevel()).isEqualTo("9"); + SetHeaderDefinition unit = (SetHeaderDefinition) split.getOutputs().get(2); + assertThat(unit.getExpression().getExpression()).isEqualTo("MILLISECONDS"); + DelayDefinition delay = (DelayDefinition) split.getOutputs().get(3); + assertThat(delay.getExpression().getExpression()).isEqualTo("5000"); + } + + @Test + void anObjectOfAClassBuiltInTheMethod() { + // Namespaces ns = new Namespaces(...): null stands in, and the rest of the route is read + JavaParseResult result = new LwJavaParser().parse(""" + from("direct:a") + .filter(xpath("/c:number = 55", ns)) + .to("mock:result"); + """); + assertThat(result.unresolved()).extracting(JavaParseResult.Unresolved::text).containsExactly("ns"); + FilterDefinition filter = (FilterDefinition) result.routes().getRoutes().get(0).getOutputs().get(0); + assertThat(filter.getExpression().getExpression()).isEqualTo("/c:number = 55"); + assertThat(filter.getOutputs()).hasSize(1); + + // xtokenize(path, 'i', ns) is the DSL method with a char, which uses ns right away + result = new LwJavaParser().parse(""" + from("direct:a").split().xtokenize("//order", 'i', ns).to("mock:order"); + """); + assertThat(result.unresolved()).extracting(JavaParseResult.Unresolved::reason) + .contains("needs a value the parser cannot see"); + } + + @Test + void modelMethodsNamedLikeTheBuilderLifeCycle() { + // configuration(...) and configure(...) are only the builder's life cycle on the route builder + JavaParseResult result = new LwJavaParser().parse(""" + from("direct:a") + .circuitBreaker().configuration("myConfig") + .to("mock:a") + .end(); + """); + assertThat(result.isComplete()).as("%s", result.unresolved()).isTrue(); + CircuitBreakerDefinition cb = (CircuitBreakerDefinition) result.routes().getRoutes().get(0).getOutputs().get(0); + assertThat(cb.getConfiguration()).isEqualTo("myConfig"); + } + @Test void globalErrorHandlingAndRest() { JavaParseResult result = new LwJavaParser().parse(""" diff --git a/docs/user-manual/modules/ROOT/pages/exception-advanced.adoc b/docs/user-manual/modules/ROOT/pages/exception-advanced.adoc index 2356967142d3..3de7dddb9746 100644 --- a/docs/user-manual/modules/ROOT/pages/exception-advanced.adoc +++ b/docs/user-manual/modules/ROOT/pages/exception-advanced.adoc @@ -425,36 +425,3 @@ public class MyRetryBean { } } ---- - -=== Using Custom ExceptionPolicyStrategy - -The default `org.apache.camel.processor.errorhandler.ExceptionPolicyStrategy` in Camel should be sufficient in nearly all use-cases. -However, if you need to use your own (use only for rare and advanced use-cases) this can be configured as the -sample below illustrates: - -._Java-only: configuring a custom ExceptionPolicyStrategy_ -[source,java] ----- -// configure the error handler to use my policy instead of the default from Camel -errorHandler(deadLetterChannel("mock:error").exceptionPolicyStrategy(new MyPolicy())); ----- - -Using our own strategy *`MyPolicy`* we can change the default behavior of -Camel with our own code to resolve which exception type -from above should be handling the given thrown exception. - -._Java-only: custom ExceptionPolicyStrategy implementation_ -[source,java] ----- -public static class MyPolicy implements ExceptionPolicyStrategy { - - @Override - public ExceptionPolicyKey getExceptionPolicy(Set<ExceptionPolicyKey> exceptionPolicies, Exchange exchange, Throwable exception) { - // This is just an example that always forces the exception type configured - // with MyPolicyException to win. - return new ExceptionPolicyKey(null, MyPolicyException.class, null); - } -} ----- - - diff --git a/docs/user-manual/modules/ROOT/pages/exception-handling-patterns.adoc b/docs/user-manual/modules/ROOT/pages/exception-handling-patterns.adoc index 8f4d84d210e0..e588166c4e80 100644 --- a/docs/user-manual/modules/ROOT/pages/exception-handling-patterns.adoc +++ b/docs/user-manual/modules/ROOT/pages/exception-handling-patterns.adoc @@ -96,7 +96,7 @@ Java:: + [source,java] ---- -onException(ValidationException) +onException(ValidationException.class) .handled(true); ---- diff --git a/docs/user-manual/modules/ROOT/pages/exception-redelivery.adoc b/docs/user-manual/modules/ROOT/pages/exception-redelivery.adoc index cdd9ffff1a31..37c244c1a78d 100644 --- a/docs/user-manual/modules/ROOT/pages/exception-redelivery.adoc +++ b/docs/user-manual/modules/ROOT/pages/exception-redelivery.adoc @@ -115,10 +115,12 @@ All redelivery attempts start at the point of the failure. So the route: [source,java] ---- onException(ConnectException.class) - .from("direct:start") + .maximumRedeliveries(3); + +from("direct:start") .process("processor1") .process("processor2") // <--- throws a ConnectException - .to("mock:theEnd") + .to("mock:theEnd"); ---- Will retry from *`processor2`* - not the complete route. diff --git a/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc b/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc index 97c4f1fc8a4d..39538fd49071 100644 --- a/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc +++ b/docs/user-manual/modules/ROOT/pages/rest-dsl-openapi.adoc @@ -255,7 +255,7 @@ Java:: + [source,java] ---- - rest().openApi("petstore-v3.json").missingOperation("ignore"); + rest().openApi().specification("petstore-v3.json").missingOperation("ignore"); ---- XML:: @@ -291,7 +291,7 @@ Java:: + [source,java] ---- - rest().openApi("petstore-v3.json").missingOperation("mock"); + rest().openApi().specification("petstore-v3.json").missingOperation("mock"); ---- XML:: @@ -346,7 +346,7 @@ Java:: ---- restConfiguration().clientRequestValidation(true); -rest().openApi("petstore-v3.json").missingOperation("mock"); +rest().openApi().specification("petstore-v3.json").missingOperation("mock"); ---- XML:: diff --git a/docs/user-manual/modules/ROOT/pages/route-template.adoc b/docs/user-manual/modules/ROOT/pages/route-template.adoc index 30ac699d67ba..a89dcad18011 100644 --- a/docs/user-manual/modules/ROOT/pages/route-template.adoc +++ b/docs/user-manual/modules/ROOT/pages/route-template.adoc @@ -501,7 +501,7 @@ public class MyRouteTemplates extends RouteBuilder { .templateParameter("color") .from("direct:{{name}}") .choice() - .when(simple("'{{color}}' == 'red'") // <1> + .when(simple("'{{color}}' == 'red'")) // <1> .to("direct:red") .otherwise() .to("color:other") diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogEndpointDslResolver.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogEndpointDslResolver.java index 1265d81d240b..502883bce5ab 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogEndpointDslResolver.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogEndpointDslResolver.java @@ -45,11 +45,12 @@ final class CatalogEndpointDslResolver implements EndpointDslResolver { return null; } String scheme = named.uri().substring(0, named.uri().indexOf("://")); - // kafka("myKafka", "orders"): a component under another name, the catalog cannot say what it is - ComponentModel model = args.size() == 2 ? null : catalog.componentModel(scheme); - if (args.size() != 2 && model == null) { + if (catalog.componentModel(scheme) == null) { + // and(user, admin) is no endpoint return null; } + // kafka("myKafka", "orders"): a component under another name, the catalog cannot say what its options are + ComponentModel model = args.size() == 2 ? null : catalog.componentModel(scheme); List<Option> resolved = new ArrayList<>(); List<String> problems = new ArrayList<>(); for (Option o : options) { diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocJavaExamplesTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocJavaExamplesTest.java new file mode 100644 index 000000000000..becc40b7badd --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocJavaExamplesTest.java @@ -0,0 +1,124 @@ +/* + * 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.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.apache.camel.java.in.JavaParseResult; +import org.apache.camel.java.in.LwJavaParser; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * CAMEL-25182: every Java route example of the documentation calls the Java DSL as it is. The examples are read with + * the Java DSL parser of camel-java-io, which replays each chain of calls against Camel's DSL without compiling the + * snippet. What the parser cannot see (lambdas, variables, helper methods) is fine; a DSL method that does not exist, + * or does not take the arguments of the example, is a broken example. + */ +class CatalogDocJavaExamplesTest { + + private static final Pattern BLOCK + = Pattern.compile("(?:^\\.(?<title>[^\\n]*)\\n)?\\[source,java\\]\\n-{4,}\\n(?<code>.*?)\\n-{4,}", + Pattern.DOTALL | Pattern.MULTILINE); + + /** A snippet with routes: blocks of plain Java (beans, configuration) are not read. */ + private static final Pattern ROUTE_DSL = Pattern.compile( + "(?<![\\w.])(from|fromF|rest|routeTemplate|onException|errorHandler|interceptFrom|intercept" + + "|interceptSendToEndpoint|onCompletion)\\s*\\("); + + /** Titles of examples that show code that is wrong on purpose, or is not Camel's Java DSL. */ + private static final List<String> TITLES_SKIPPED + = List.of("pseudo", "incorrect", "will not compile", "does not compile", "before migration", "old "); + + /** The reasons of the parser that mean the example calls the DSL wrongly. */ + private static final List<String> BROKEN = List.of( + "no such DSL method", "arguments the DSL method does not take", "the DSL refused it"); + + /** Where the user manual is in the source tree; its pages are not in the catalog. */ + private static final Path USER_MANUAL = Path.of("../../../docs/user-manual/modules/ROOT/pages"); + + /** User manual pages that show old syntax on purpose. */ + private static final List<String> USER_MANUAL_SKIPPED = List.of("upgrade-guide", "migration-guide"); + + private static CamelCatalog catalog; + + @BeforeAll + static void setup() { + catalog = new DefaultCamelCatalog(); + } + + @Test + void everyJavaExampleOfTheDocumentationCallsTheJavaDsl() throws IOException { + List<String> failures = new ArrayList<>(); + int examples = 0; + for (String name : catalog.findDocNames()) { + examples += check(name, catalog.asciiDoc(name), failures); + } + if (Files.isDirectory(USER_MANUAL)) { + try (Stream<Path> files = Files.list(USER_MANUAL)) { + for (Path f : files.filter(f -> f.toString().endsWith(".adoc")).sorted().toList()) { + String name = f.getFileName().toString().replace(".adoc", ""); + if (USER_MANUAL_SKIPPED.stream().noneMatch(name::contains)) { + examples += check(name, Files.readString(f), failures); + } + } + } + } + assertThat(examples).as("Java route examples found in the documentation").isGreaterThan(2000); + assertThat(failures).as("Documentation examples that call the Java DSL wrongly").isEmpty(); + } + + private static int check(String page, String doc, List<String> failures) { + if (doc == null) { + return 0; + } + int examples = 0; + Matcher m = BLOCK.matcher(doc); + while (m.find()) { + String title = m.group("title") != null ? m.group("title").toLowerCase(Locale.ROOT) : ""; + String code = m.group("code"); + if (!ROUTE_DSL.matcher(code).find() || TITLES_SKIPPED.stream().anyMatch(title::contains)) { + continue; + } + examples++; + int line = 1 + (int) doc.substring(0, m.start("code")).chars().filter(c -> c == '\n').count(); + JavaParseResult result = new LwJavaParser() + .setEndpointDslResolver(new CatalogEndpointDslResolver(catalog)) + .setConstantResolver(new ProjectConstantResolver(Map.of(), catalog)) + .parse(code); + for (JavaParseResult.Unresolved u : result.unresolved()) { + if (BROKEN.stream().anyMatch(u.reason()::startsWith)) { + failures.add(page + ":" + (line + u.line() - 1) + " " + u.text() + " - " + u.reason()); + } + } + } + return examples; + } +}
