This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-24710-catalog-checks in repository https://gitbox.apache.org/repos/asf/camel.git
commit 045d7166dcd7ecf83088eebf7984aed13aab7ca6 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 16 10:07:25 2026 +0200 CAMEL-24710: docs - the YAML examples pass the catalog checks too, guarded by CatalogDocExamplesTest in camel-jbang-core: the checks read a comment after a value, a block scalar and a nested map in parameters:, a dotted or prefixed header name and name: only under setHeader; the catalog validator accepts option.key for a Map option; crypto, solr and box declare the headers their examples use; xmlSecurity, metrics, fop and openai examples corrected Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../org/apache/camel/catalog/components/box.json | 3 + .../apache/camel/catalog/components/crypto.json | 5 +- .../org/apache/camel/catalog/components/solr.json | 4 +- .../apache/camel/catalog/docs/fop-component.adoc | 8 +- .../camel/catalog/docs/metrics-component.adoc | 8 +- .../camel/catalog/docs/openai-component.adoc | 16 +-- .../camel/catalog/docs/xmlSecurity-dataformat.adoc | 20 ++-- .../org/apache/camel/catalog/CamelCatalogTest.java | 13 +++ .../src/main/docs/openai-component.adoc | 16 +-- .../org/apache/camel/component/box/box.json | 3 + .../apache/camel/component/box/BoxEndpoint.java | 3 +- .../camel/component/box/internal/BoxConstants.java | 7 +- .../org/apache/camel/component/crypto/crypto.json | 5 +- .../crypto/DigitalSignatureConstants.java | 3 + .../camel-fop/src/main/docs/fop-component.adoc | 8 +- .../src/main/docs/metrics-component.adoc | 8 +- .../org/apache/camel/component/solr/solr.json | 4 +- .../apache/camel/component/solr/SolrConstants.java | 6 + .../src/main/docs/xmlSecurity-dataformat.adoc | 20 ++-- .../camel/catalog/impl/AbstractCamelCatalog.java | 18 ++- .../dsl/jbang/core/commands/ai/EndpointChecks.java | 30 ++++- .../dsl/jbang/core/commands/ai/HeaderChecks.java | 54 ++++++++- .../dsl/jbang/core/commands/ai/YamlLines.java | 27 +++++ .../core/commands/ai/CatalogDocExamplesTest.java | 128 +++++++++++++++++++++ .../commands/ai/SourceValidatorEndpointTest.java | 58 ++++++++++ .../dsl/yaml/validator/EipDocExamplesTest.java | 11 +- 26 files changed, 411 insertions(+), 75 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/box.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/box.json index 7327874b58e2..f17f0102eb31 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/box.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/box.json @@ -45,6 +45,9 @@ "userName": { "index": 17, "kind": "property", "displayName": "User Name", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.box.BoxConfiguration", "configurationField": "configuration", "description": "Box user name, MUST be provided" }, "userPassword": { "index": 18, "kind": "property", "displayName": "User Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.box.BoxConfiguration", "configurationField": "configuration", "description": "Box user password, MUST be provided if authSecureStorage is not set, or returns null on [...] }, + "headers": { + "CamelBox.": { "index": 0, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The prefix of the headers that carry the parameters of the API method: the header CamelBox.fileName holds the fileName parameter.", "constantName": "org.apache.camel.component.box.internal.BoxConstants#PROPERTY_PREFIX" } + }, "properties": { "apiName": { "index": 0, "kind": "path", "displayName": "Api Name", "group": "common", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.box.internal.BoxApiName", "enum": [ "COLLABORATIONS", "COMMENTS", "EVENT_LOGS", "FILES", "FOLDERS", "GROUPS", "EVENTS", "SEARCH", "TASKS", "USERS" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.box.BoxConfiguration", "configura [...] "methodName": { "index": 1, "kind": "path", "displayName": "Method Name", "group": "common", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.box.BoxConfiguration", "configurationField": "configuration", "description": "What sub operation to use for the selected operation" }, diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/crypto.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/crypto.json index 17f736dc943c..9f14e382ba45 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/crypto.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/crypto.json @@ -49,8 +49,9 @@ "headers": { "CamelSignaturePrivateKey": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.security.PrivateKey", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The PrivateKey that should be used to sign the message", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstants#SIGNATURE_PRIVATE_KEY" }, "CamelSignaturePublicKeyOrCert": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Certificate or PublicKey", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Certificate or PublicKey that should be used to verify the signature", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstants#SIGNATURE_PUBLIC_KEY_OR_CERT" }, - "CamelSignatureKeyStoreAlias": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The alias used to query the KeyStore for keys and Certificates to be used in signing and verifying exchanges", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstants#KEYSTORE_ALIAS" }, - "CamelSignatureKeyStorePassword": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "char[]", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The password used to access an aliased PrivateKey in the KeyStore.", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstants#KEYSTORE_PASSWORD" } + "CamelDigitalSignature": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "byte[] (Base64 encoded)", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The signature of the message, set by the sign operation and read by the verify operation (the signatureHeaderName option changes the header name)", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstant [...] + "CamelSignatureKeyStoreAlias": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The alias used to query the KeyStore for keys and Certificates to be used in signing and verifying exchanges", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstants#KEYSTORE_ALIAS" }, + "CamelSignatureKeyStorePassword": { "index": 4, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "char[]", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The password used to access an aliased PrivateKey in the KeyStore.", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstants#KEYSTORE_PASSWORD" } }, "properties": { "cryptoOperation": { "index": 0, "kind": "path", "displayName": "Crypto Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.crypto.CryptoOperation", "enum": [ "sign", "verify" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.crypto.DigitalSignatureConfiguration", "configurationField": "configuration", "description": "Set the Crypto o [...] diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/solr.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/solr.json index 97c66fc517d0..299fa0fe908c 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/solr.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/solr.json @@ -47,7 +47,9 @@ "CamelSolrFrom": { "index": 5, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The starting index of the response.", "constantName": "org.apache.camel.component.solr.SolrConstants#PARAM_FROM" }, "CamelSolrParams": { "index": 6, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "org.apache.solr.common.params.SolrParams", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The solr parameters to use for the request.", "constantName": "org.apache.camel.component.solr.SolrConstants#PARAM_SOLR_PARAMS" }, "CamelSolrDeleteByQuery": { "index": 7, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "false", "description": "For the delete instruction, interpret body as query\/queries instead of id\/ids.", "constantName": "org.apache.camel.component.solr.SolrConstants#PARAM_DELETE_BY_QUERY" }, - "Content-Type": { "index": 8, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The content type is used to identify the type when inserting files.", "constantName": "org.apache.camel.component.solr.SolrConstants#PARAM_CONTENT_TYPE" } + "Content-Type": { "index": 8, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The content type is used to identify the type when inserting files.", "constantName": "org.apache.camel.component.solr.SolrConstants#PARAM_CONTENT_TYPE" }, + "CamelSolrField.": { "index": 9, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The prefix of the headers that carry the fields of the document to insert: the header CamelSolrField.id holds the id field.", "constantName": "org.apache.camel.component.solr.SolrConstants#HEADER_FIELD_PREFIX" }, + "CamelSolrParam.": { "index": 10, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The prefix of the headers that carry extra request parameters: the header CamelSolrParam.commit holds the commit parameter.", "constantName": "org.apache.camel.component.solr.SolrConstants#HEADER_PARAM_PREFIX" } }, "properties": { "host": { "index": 0, "kind": "path", "displayName": "Host", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.solr.SolrConfiguration", "configurationField": "configuration", "description": "The solr instance host name (set to 'default' to use the host name defined on component level)" }, diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/fop-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/fop-component.adoc index 4b8140c6875c..d71438459ecb 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/fop-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/fop-component.adoc @@ -72,6 +72,8 @@ supported: The complete list of valid output formats can be found in the `MimeConstants.java` source file. +The endpoint URI takes either the name in lower case (`fop:pdf`) or the output format (`fop:application/pdf`). + // component options: START include::partial$component-configure-options.adoc[] @@ -137,7 +139,7 @@ Java:: ---- from("file:source/data/xml") .to("xslt:xslt/template.xsl") - .to("fop:application/pdf") + .to("fop:pdf") .to("file:target/data"); ---- @@ -148,7 +150,7 @@ XML:: <route> <from uri="file:source/data/xml"/> <to uri="xslt:xslt/template.xsl"/> - <to uri="fop:application/pdf"/> + <to uri="fop:pdf"/> <to uri="file:target/data"/> </route> ---- @@ -164,7 +166,7 @@ YAML:: - to: uri: xslt:xslt/template.xsl - to: - uri: fop:application/pdf + uri: fop:pdf - to: uri: file:target/data ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/metrics-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/metrics-component.adoc index a70cb8c85cbc..e84bfe571880 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/metrics-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/metrics-component.adoc @@ -543,9 +543,9 @@ Java:: + [source,java] ---- -// marks simple.meter without value +// marks the simple.meter meter without value from("direct:in") - .to("metrics:simple.meter") + .to("metrics:meter:simple.meter") .to("direct:out"); ---- @@ -555,7 +555,7 @@ XML:: ---- <route> <from uri="direct:in"/> - <to uri="metrics:simple.meter"/> + <to uri="metrics:meter:simple.meter"/> <to uri="direct:out"/> </route> ---- @@ -569,7 +569,7 @@ YAML:: uri: direct:in steps: - to: - uri: metrics:simple.meter + uri: metrics:meter:simple.meter - to: uri: direct:out ---- diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc index 79bd9c3949fa..039ce1f67b1b 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/openai-component.adoc @@ -1046,9 +1046,9 @@ Java:: ---- from("direct:chat") .to("openai:chat-completion?model=qwen3" - + "&additionalResponseHeader.reasoning_content=CamelMyReasoning" - + "&additionalResponseHeader.custom_field=CamelMyCustomField") - .log("Custom reasoning: ${header.CamelMyReasoning}"); + + "&additionalResponseHeader.reasoning_content=MyReasoning" + + "&additionalResponseHeader.custom_field=MyCustomField") + .log("Custom reasoning: ${header.MyReasoning}"); ---- XML:: @@ -1057,8 +1057,8 @@ XML:: ---- <route> <from uri="direct:chat"/> - <to uri="openai:chat-completion?model=qwen3&additionalResponseHeader.reasoning_content=CamelMyReasoning&additionalResponseHeader.custom_field=CamelMyCustomField"/> - <log message="Custom reasoning: ${header.CamelMyReasoning}"/> + <to uri="openai:chat-completion?model=qwen3&additionalResponseHeader.reasoning_content=MyReasoning&additionalResponseHeader.custom_field=MyCustomField"/> + <log message="Custom reasoning: ${header.MyReasoning}"/> </route> ---- @@ -1074,10 +1074,10 @@ YAML:: uri: openai:chat-completion parameters: model: qwen3 - additionalResponseHeader.reasoning_content: CamelMyReasoning - additionalResponseHeader.custom_field: CamelMyCustomField + additionalResponseHeader.reasoning_content: MyReasoning + additionalResponseHeader.custom_field: MyCustomField - log: - message: "Custom reasoning: ${header.CamelMyReasoning}" + message: "Custom reasoning: ${header.MyReasoning}" ---- ==== 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 eaf5a86d81c7..ced411c85754 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 @@ -208,7 +208,7 @@ Java:: ---- from("direct:start") .marshal().xmlSecurity("//cheese:cheesesites/italy", namespaces, true, - "recipient", XMLCipher.AES_128_CBC, XMLCipher.RSA_v1dot5, trustStoreParams) + "recipient", XMLCipher.AES_128, XMLCipher.RSA_v1dot5, trustStoreParams) .to("..."); ---- @@ -227,8 +227,8 @@ XML:: <marshal> <xmlSecurity secureTag="//cheese:cheesesites/italy" secureTagContents="true" - xmlCipherAlgorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc" - keyCipherAlgorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" + xmlCipherAlgorithm="AES_128" + keyCipherAlgorithm="RSA_v1dot5" recipientKeyAlias="recipient" keyOrTrustStoreParameters="trustStoreParams"/> </marshal> @@ -247,8 +247,8 @@ YAML:: xmlSecurity: secureTag: "//cheese:cheesesites/italy" secureTagContents: true - xmlCipherAlgorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc" - keyCipherAlgorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5" + xmlCipherAlgorithm: AES_128 + keyCipherAlgorithm: RSA_v1dot5 recipientKeyAlias: recipient keyOrTrustStoreParameters: "#trustStoreParams" ---- @@ -265,7 +265,7 @@ Java:: ---- from("direct:encrypted") .unmarshal().xmlSecurity("//cheese:cheesesites/italy", namespaces, true, - "recipient", XMLCipher.AES_128_CBC, XMLCipher.RSA_v1dot5, keyStoreParams) + "recipient", XMLCipher.AES_128, XMLCipher.RSA_v1dot5, keyStoreParams) .to("..."); ---- @@ -284,8 +284,8 @@ XML:: <unmarshal> <xmlSecurity secureTag="//cheese:cheesesites/italy" secureTagContents="true" - xmlCipherAlgorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc" - keyCipherAlgorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" + xmlCipherAlgorithm="AES_128" + keyCipherAlgorithm="RSA_v1dot5" recipientKeyAlias="recipient" keyOrTrustStoreParameters="keyStoreParams" keyPassword="privateKeyPassword" /> @@ -305,8 +305,8 @@ YAML:: xmlSecurity: secureTag: "//cheese:cheesesites/italy" secureTagContents: true - xmlCipherAlgorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc" - keyCipherAlgorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5" + xmlCipherAlgorithm: AES_128 + keyCipherAlgorithm: RSA_v1dot5 recipientKeyAlias: recipient keyOrTrustStoreParameters: "#keyStoreParams" keyPassword: privateKeyPassword diff --git a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java index 4fd62a61fc57..e5a7079f4a03 100644 --- a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java +++ b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java @@ -593,6 +593,19 @@ public class CamelCatalogTest { assertEquals("beer", map.get("myPrefix.baz")); } + @Test + public void validateMapOptionEntries() { + // userMetadata is a Map option: userMetadata.key=value fills an entry of the map, as property binding does + EndpointValidationResult result = catalog.validateEndpointProperties( + "spring-ai-chat:assistant?chatModel=#myModel&userMetadata.messageId=abc&userMetadata.priority=high"); + assertTrue(result.isSuccess(), result.summaryErrorMessage(false)); + + // the option before the dot must be a Map + result = catalog.validateEndpointProperties("spring-ai-chat:assistant?chatModel=#myModel&chatModel.foo=abc"); + assertFalse(result.isSuccess()); + assertTrue(result.getUnknown().contains("chatModel.foo")); + } + @Test public void testEndpointPropertiesPlaceholders() throws Exception { Map<String, String> map = catalog.endpointProperties("timer:foo?period={{howoften}}&repeatCount=5"); diff --git a/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc b/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc index 79bd9c3949fa..039ce1f67b1b 100644 --- a/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc +++ b/components/camel-ai/camel-openai/src/main/docs/openai-component.adoc @@ -1046,9 +1046,9 @@ Java:: ---- from("direct:chat") .to("openai:chat-completion?model=qwen3" - + "&additionalResponseHeader.reasoning_content=CamelMyReasoning" - + "&additionalResponseHeader.custom_field=CamelMyCustomField") - .log("Custom reasoning: ${header.CamelMyReasoning}"); + + "&additionalResponseHeader.reasoning_content=MyReasoning" + + "&additionalResponseHeader.custom_field=MyCustomField") + .log("Custom reasoning: ${header.MyReasoning}"); ---- XML:: @@ -1057,8 +1057,8 @@ XML:: ---- <route> <from uri="direct:chat"/> - <to uri="openai:chat-completion?model=qwen3&additionalResponseHeader.reasoning_content=CamelMyReasoning&additionalResponseHeader.custom_field=CamelMyCustomField"/> - <log message="Custom reasoning: ${header.CamelMyReasoning}"/> + <to uri="openai:chat-completion?model=qwen3&additionalResponseHeader.reasoning_content=MyReasoning&additionalResponseHeader.custom_field=MyCustomField"/> + <log message="Custom reasoning: ${header.MyReasoning}"/> </route> ---- @@ -1074,10 +1074,10 @@ YAML:: uri: openai:chat-completion parameters: model: qwen3 - additionalResponseHeader.reasoning_content: CamelMyReasoning - additionalResponseHeader.custom_field: CamelMyCustomField + additionalResponseHeader.reasoning_content: MyReasoning + additionalResponseHeader.custom_field: MyCustomField - log: - message: "Custom reasoning: ${header.CamelMyReasoning}" + message: "Custom reasoning: ${header.MyReasoning}" ---- ==== diff --git a/components/camel-box/camel-box-component/src/generated/resources/META-INF/org/apache/camel/component/box/box.json b/components/camel-box/camel-box-component/src/generated/resources/META-INF/org/apache/camel/component/box/box.json index 7327874b58e2..f17f0102eb31 100644 --- a/components/camel-box/camel-box-component/src/generated/resources/META-INF/org/apache/camel/component/box/box.json +++ b/components/camel-box/camel-box-component/src/generated/resources/META-INF/org/apache/camel/component/box/box.json @@ -45,6 +45,9 @@ "userName": { "index": 17, "kind": "property", "displayName": "User Name", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.box.BoxConfiguration", "configurationField": "configuration", "description": "Box user name, MUST be provided" }, "userPassword": { "index": 18, "kind": "property", "displayName": "User Password", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "security": "secret", "configurationClass": "org.apache.camel.component.box.BoxConfiguration", "configurationField": "configuration", "description": "Box user password, MUST be provided if authSecureStorage is not set, or returns null on [...] }, + "headers": { + "CamelBox.": { "index": 0, "kind": "header", "displayName": "", "group": "common", "label": "", "required": false, "javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The prefix of the headers that carry the parameters of the API method: the header CamelBox.fileName holds the fileName parameter.", "constantName": "org.apache.camel.component.box.internal.BoxConstants#PROPERTY_PREFIX" } + }, "properties": { "apiName": { "index": 0, "kind": "path", "displayName": "Api Name", "group": "common", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.box.internal.BoxApiName", "enum": [ "COLLABORATIONS", "COMMENTS", "EVENT_LOGS", "FILES", "FOLDERS", "GROUPS", "EVENTS", "SEARCH", "TASKS", "USERS" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.box.BoxConfiguration", "configura [...] "methodName": { "index": 1, "kind": "path", "displayName": "Method Name", "group": "common", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.box.BoxConfiguration", "configurationField": "configuration", "description": "What sub operation to use for the selected operation" }, diff --git a/components/camel-box/camel-box-component/src/main/java/org/apache/camel/component/box/BoxEndpoint.java b/components/camel-box/camel-box-component/src/main/java/org/apache/camel/component/box/BoxEndpoint.java index c533c6c67efb..66e9231ef9f2 100644 --- a/components/camel-box/camel-box-component/src/main/java/org/apache/camel/component/box/BoxEndpoint.java +++ b/components/camel-box/camel-box-component/src/main/java/org/apache/camel/component/box/BoxEndpoint.java @@ -50,7 +50,8 @@ import org.apache.camel.support.component.ApiMethodPropertiesHelper; */ @UriEndpoint(firstVersion = "2.14.0", scheme = "box", title = "Box", syntax = "box:apiName/methodName", apiSyntax = "apiName/methodName", - consumerPrefix = "consumer", category = { Category.CLOUD, Category.FILE, Category.API }, lenientProperties = true) + consumerPrefix = "consumer", category = { Category.CLOUD, Category.FILE, Category.API }, lenientProperties = true, + headersClass = BoxConstants.class) @Metadata(excludeProperties = "startScheduler,initialDelay,delay,timeUnit,useFixedDelay,pollStrategy,runLoggingLevel,sendEmptyMessageWhenIdle" + ",greedy,scheduler,schedulerProperties,scheduledExecutorService,backoffMultiplier,backoffIdleThreshold,backoffErrorThreshold,repeatCount,bridgeErrorHandler") public class BoxEndpoint extends AbstractApiEndpoint<BoxApiName, BoxConfiguration> { diff --git a/components/camel-box/camel-box-component/src/main/java/org/apache/camel/component/box/internal/BoxConstants.java b/components/camel-box/camel-box-component/src/main/java/org/apache/camel/component/box/internal/BoxConstants.java index 61223d730729..71653b33851a 100644 --- a/components/camel-box/camel-box-component/src/main/java/org/apache/camel/component/box/internal/BoxConstants.java +++ b/components/camel-box/camel-box-component/src/main/java/org/apache/camel/component/box/internal/BoxConstants.java @@ -16,12 +16,17 @@ */ package org.apache.camel.component.box.internal; +import org.apache.camel.spi.Metadata; + /** * Constants for Box component. */ public interface BoxConstants { - // suffix for parameters when passed as exchange header properties + // prefix for parameters when passed as exchange header properties + @Metadata(description = "The prefix of the headers that carry the parameters of the API method: the header" + + " CamelBox.fileName holds the fileName parameter.", + javaType = "Object") String PROPERTY_PREFIX = "CamelBox."; // thread profile name for this component diff --git a/components/camel-crypto/src/generated/resources/META-INF/org/apache/camel/component/crypto/crypto.json b/components/camel-crypto/src/generated/resources/META-INF/org/apache/camel/component/crypto/crypto.json index 17f736dc943c..9f14e382ba45 100644 --- a/components/camel-crypto/src/generated/resources/META-INF/org/apache/camel/component/crypto/crypto.json +++ b/components/camel-crypto/src/generated/resources/META-INF/org/apache/camel/component/crypto/crypto.json @@ -49,8 +49,9 @@ "headers": { "CamelSignaturePrivateKey": { "index": 0, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "java.security.PrivateKey", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The PrivateKey that should be used to sign the message", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstants#SIGNATURE_PRIVATE_KEY" }, "CamelSignaturePublicKeyOrCert": { "index": 1, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Certificate or PublicKey", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The Certificate or PublicKey that should be used to verify the signature", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstants#SIGNATURE_PUBLIC_KEY_OR_CERT" }, - "CamelSignatureKeyStoreAlias": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The alias used to query the KeyStore for keys and Certificates to be used in signing and verifying exchanges", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstants#KEYSTORE_ALIAS" }, - "CamelSignatureKeyStorePassword": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "char[]", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The password used to access an aliased PrivateKey in the KeyStore.", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstants#KEYSTORE_PASSWORD" } + "CamelDigitalSignature": { "index": 2, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "byte[] (Base64 encoded)", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The signature of the message, set by the sign operation and read by the verify operation (the signatureHeaderName option changes the header name)", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstant [...] + "CamelSignatureKeyStoreAlias": { "index": 3, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The alias used to query the KeyStore for keys and Certificates to be used in signing and verifying exchanges", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstants#KEYSTORE_ALIAS" }, + "CamelSignatureKeyStorePassword": { "index": 4, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "char[]", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The password used to access an aliased PrivateKey in the KeyStore.", "constantName": "org.apache.camel.component.crypto.DigitalSignatureConstants#KEYSTORE_PASSWORD" } }, "properties": { "cryptoOperation": { "index": 0, "kind": "path", "displayName": "Crypto Operation", "group": "producer", "label": "", "required": true, "type": "enum", "javaType": "org.apache.camel.component.crypto.CryptoOperation", "enum": [ "sign", "verify" ], "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.crypto.DigitalSignatureConfiguration", "configurationField": "configuration", "description": "Set the Crypto o [...] diff --git a/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/DigitalSignatureConstants.java b/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/DigitalSignatureConstants.java index d94677d737b4..8dea9fda1d68 100644 --- a/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/DigitalSignatureConstants.java +++ b/components/camel-crypto/src/main/java/org/apache/camel/component/crypto/DigitalSignatureConstants.java @@ -27,6 +27,9 @@ public final class DigitalSignatureConstants { @Metadata(description = "The Certificate or PublicKey that should be used to verify the signature", javaType = "Certificate or PublicKey") public static final String SIGNATURE_PUBLIC_KEY_OR_CERT = "CamelSignaturePublicKeyOrCert"; + @Metadata(description = "The signature of the message, set by the sign operation and read by the verify operation" + + " (the signatureHeaderName option changes the header name)", + javaType = "byte[] (Base64 encoded)") public static final String SIGNATURE = "CamelDigitalSignature"; @Metadata(description = "The alias used to query the KeyStore for keys and Certificates to be\n" + " used in signing and verifying exchanges", diff --git a/components/camel-fop/src/main/docs/fop-component.adoc b/components/camel-fop/src/main/docs/fop-component.adoc index 4b8140c6875c..d71438459ecb 100644 --- a/components/camel-fop/src/main/docs/fop-component.adoc +++ b/components/camel-fop/src/main/docs/fop-component.adoc @@ -72,6 +72,8 @@ supported: The complete list of valid output formats can be found in the `MimeConstants.java` source file. +The endpoint URI takes either the name in lower case (`fop:pdf`) or the output format (`fop:application/pdf`). + // component options: START include::partial$component-configure-options.adoc[] @@ -137,7 +139,7 @@ Java:: ---- from("file:source/data/xml") .to("xslt:xslt/template.xsl") - .to("fop:application/pdf") + .to("fop:pdf") .to("file:target/data"); ---- @@ -148,7 +150,7 @@ XML:: <route> <from uri="file:source/data/xml"/> <to uri="xslt:xslt/template.xsl"/> - <to uri="fop:application/pdf"/> + <to uri="fop:pdf"/> <to uri="file:target/data"/> </route> ---- @@ -164,7 +166,7 @@ YAML:: - to: uri: xslt:xslt/template.xsl - to: - uri: fop:application/pdf + uri: fop:pdf - to: uri: file:target/data ---- diff --git a/components/camel-metrics/src/main/docs/metrics-component.adoc b/components/camel-metrics/src/main/docs/metrics-component.adoc index a70cb8c85cbc..e84bfe571880 100644 --- a/components/camel-metrics/src/main/docs/metrics-component.adoc +++ b/components/camel-metrics/src/main/docs/metrics-component.adoc @@ -543,9 +543,9 @@ Java:: + [source,java] ---- -// marks simple.meter without value +// marks the simple.meter meter without value from("direct:in") - .to("metrics:simple.meter") + .to("metrics:meter:simple.meter") .to("direct:out"); ---- @@ -555,7 +555,7 @@ XML:: ---- <route> <from uri="direct:in"/> - <to uri="metrics:simple.meter"/> + <to uri="metrics:meter:simple.meter"/> <to uri="direct:out"/> </route> ---- @@ -569,7 +569,7 @@ YAML:: uri: direct:in steps: - to: - uri: metrics:simple.meter + uri: metrics:meter:simple.meter - to: uri: direct:out ---- diff --git a/components/camel-solr/src/generated/resources/META-INF/org/apache/camel/component/solr/solr.json b/components/camel-solr/src/generated/resources/META-INF/org/apache/camel/component/solr/solr.json index 97c66fc517d0..299fa0fe908c 100644 --- a/components/camel-solr/src/generated/resources/META-INF/org/apache/camel/component/solr/solr.json +++ b/components/camel-solr/src/generated/resources/META-INF/org/apache/camel/component/solr/solr.json @@ -47,7 +47,9 @@ "CamelSolrFrom": { "index": 5, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Integer", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The starting index of the response.", "constantName": "org.apache.camel.component.solr.SolrConstants#PARAM_FROM" }, "CamelSolrParams": { "index": 6, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "org.apache.solr.common.params.SolrParams", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The solr parameters to use for the request.", "constantName": "org.apache.camel.component.solr.SolrConstants#PARAM_SOLR_PARAMS" }, "CamelSolrDeleteByQuery": { "index": 7, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "boolean", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "defaultValue": "false", "description": "For the delete instruction, interpret body as query\/queries instead of id\/ids.", "constantName": "org.apache.camel.component.solr.SolrConstants#PARAM_DELETE_BY_QUERY" }, - "Content-Type": { "index": 8, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The content type is used to identify the type when inserting files.", "constantName": "org.apache.camel.component.solr.SolrConstants#PARAM_CONTENT_TYPE" } + "Content-Type": { "index": 8, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The content type is used to identify the type when inserting files.", "constantName": "org.apache.camel.component.solr.SolrConstants#PARAM_CONTENT_TYPE" }, + "CamelSolrField.": { "index": 9, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "Object", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The prefix of the headers that carry the fields of the document to insert: the header CamelSolrField.id holds the id field.", "constantName": "org.apache.camel.component.solr.SolrConstants#HEADER_FIELD_PREFIX" }, + "CamelSolrParam.": { "index": 10, "kind": "header", "displayName": "", "group": "producer", "label": "", "required": false, "javaType": "String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "The prefix of the headers that carry extra request parameters: the header CamelSolrParam.commit holds the commit parameter.", "constantName": "org.apache.camel.component.solr.SolrConstants#HEADER_PARAM_PREFIX" } }, "properties": { "host": { "index": 0, "kind": "path", "displayName": "Host", "group": "producer", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.solr.SolrConfiguration", "configurationField": "configuration", "description": "The solr instance host name (set to 'default' to use the host name defined on component level)" }, diff --git a/components/camel-solr/src/main/java/org/apache/camel/component/solr/SolrConstants.java b/components/camel-solr/src/main/java/org/apache/camel/component/solr/SolrConstants.java index d5bfc3073005..1a786de7f013 100644 --- a/components/camel-solr/src/main/java/org/apache/camel/component/solr/SolrConstants.java +++ b/components/camel-solr/src/main/java/org/apache/camel/component/solr/SolrConstants.java @@ -42,7 +42,13 @@ public interface SolrConstants { @Metadata(description = "The content type is used to identify the type when inserting files.", javaType = "String") String PARAM_CONTENT_TYPE = Exchange.CONTENT_TYPE; + @Metadata(description = "The prefix of the headers that carry the fields of the document to insert: the header" + + " CamelSolrField.id holds the id field.", + javaType = "Object") String HEADER_FIELD_PREFIX = "CamelSolrField."; + @Metadata(description = "The prefix of the headers that carry extra request parameters: the header" + + " CamelSolrParam.commit holds the commit parameter.", + javaType = "String") String HEADER_PARAM_PREFIX = "CamelSolrParam."; String PROPERTY_ACTION_CONTEXT = "SolrActionContext"; diff --git a/components/camel-xmlsecurity/src/main/docs/xmlSecurity-dataformat.adoc b/components/camel-xmlsecurity/src/main/docs/xmlSecurity-dataformat.adoc index eaf5a86d81c7..ced411c85754 100644 --- a/components/camel-xmlsecurity/src/main/docs/xmlSecurity-dataformat.adoc +++ b/components/camel-xmlsecurity/src/main/docs/xmlSecurity-dataformat.adoc @@ -208,7 +208,7 @@ Java:: ---- from("direct:start") .marshal().xmlSecurity("//cheese:cheesesites/italy", namespaces, true, - "recipient", XMLCipher.AES_128_CBC, XMLCipher.RSA_v1dot5, trustStoreParams) + "recipient", XMLCipher.AES_128, XMLCipher.RSA_v1dot5, trustStoreParams) .to("..."); ---- @@ -227,8 +227,8 @@ XML:: <marshal> <xmlSecurity secureTag="//cheese:cheesesites/italy" secureTagContents="true" - xmlCipherAlgorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc" - keyCipherAlgorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" + xmlCipherAlgorithm="AES_128" + keyCipherAlgorithm="RSA_v1dot5" recipientKeyAlias="recipient" keyOrTrustStoreParameters="trustStoreParams"/> </marshal> @@ -247,8 +247,8 @@ YAML:: xmlSecurity: secureTag: "//cheese:cheesesites/italy" secureTagContents: true - xmlCipherAlgorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc" - keyCipherAlgorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5" + xmlCipherAlgorithm: AES_128 + keyCipherAlgorithm: RSA_v1dot5 recipientKeyAlias: recipient keyOrTrustStoreParameters: "#trustStoreParams" ---- @@ -265,7 +265,7 @@ Java:: ---- from("direct:encrypted") .unmarshal().xmlSecurity("//cheese:cheesesites/italy", namespaces, true, - "recipient", XMLCipher.AES_128_CBC, XMLCipher.RSA_v1dot5, keyStoreParams) + "recipient", XMLCipher.AES_128, XMLCipher.RSA_v1dot5, keyStoreParams) .to("..."); ---- @@ -284,8 +284,8 @@ XML:: <unmarshal> <xmlSecurity secureTag="//cheese:cheesesites/italy" secureTagContents="true" - xmlCipherAlgorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc" - keyCipherAlgorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" + xmlCipherAlgorithm="AES_128" + keyCipherAlgorithm="RSA_v1dot5" recipientKeyAlias="recipient" keyOrTrustStoreParameters="keyStoreParams" keyPassword="privateKeyPassword" /> @@ -305,8 +305,8 @@ YAML:: xmlSecurity: secureTag: "//cheese:cheesesites/italy" secureTagContents: true - xmlCipherAlgorithm: "http://www.w3.org/2001/04/xmlenc#aes128-cbc" - keyCipherAlgorithm: "http://www.w3.org/2001/04/xmlenc#rsa-1_5" + xmlCipherAlgorithm: AES_128 + keyCipherAlgorithm: RSA_v1dot5 recipientKeyAlias: recipient keyOrTrustStoreParameters: "#keyStoreParams" keyPassword: privateKeyPassword diff --git a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java index 2f44c34ceb89..957c6e06c0a9 100644 --- a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java +++ b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java @@ -305,7 +305,7 @@ public abstract class AbstractCamelCatalog { boolean lookup = value.startsWith("#") && value.length() > 1; // we cannot evaluate multi values as strict as the others, as we don't know their expected types boolean multiValue = prefix != null && originalName.startsWith(prefix) - && row.isMultiValue(); + && row.isMultiValue() || isMapEntry(row, originalName); // default value Object defaultValue = row.getDefaultValue(); @@ -1844,9 +1844,25 @@ public abstract class AbstractCamelCatalog { return row.getName(); } } + int dot = name.indexOf('.'); + if (dot > 0) { + BaseOptionModel row = rows.get(name.substring(0, dot)); + if (row != null && isMapEntry(row, name)) { + return row.getName(); + } + } return null; } + /** + * Whether the name sets an entry of a Map option (userMetadata.messageId=x fills the userMetadata map), as property + * binding does. + */ + private static boolean isMapEntry(BaseOptionModel row, String name) { + String javaType = row.getJavaType(); + return javaType != null && javaType.startsWith("java.util.Map") && name.startsWith(row.getName() + "."); + } + /** * Converts the string from dash format into camel case (hello-great-world -> helloGreatWorld) * diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java index f992dd13de86..3d93405485e4 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java @@ -32,6 +32,7 @@ import static org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.YAML_URI_PAT import static org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.countLeadingSpaces; import static org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.extractEipFromLine; import static org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.findParentEip; +import static org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.stripComment; import static org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.unquote; /** @@ -148,6 +149,9 @@ final class EndpointChecks { String nextTrimmed = next.trim(); if (nextIndent == lineIndent && nextTrimmed.startsWith("parameters:")) { int paramBlockIndent = nextIndent; + int blockScalarIndent = -1; + String mapKey = null; + int mapIndent = -1; for (int k = j + 1; k < lines.length; k++) { String paramLine = lines[k]; if (paramLine.isBlank()) { @@ -157,11 +161,33 @@ final class EndpointChecks { if (paramIndent <= paramBlockIndent) { break; } + if (blockScalarIndent >= 0 && paramIndent > blockScalarIndent) { + // the lines of a block scalar (argSchema: | followed by JSON) are its value, not options + continue; + } + blockScalarIndent = -1; + if (mapKey != null && paramIndent <= mapIndent) { + mapKey = null; + } String paramTrimmed = paramLine.trim(); int colonPos = paramTrimmed.indexOf(':'); if (colonPos > 0) { - String key = paramTrimmed.substring(0, colonPos).trim(); - String val = unquote(paramTrimmed.substring(colonPos + 1).trim()); + String key = unquote(paramTrimmed.substring(0, colonPos).trim()); + String val = unquote(stripComment(paramTrimmed.substring(colonPos + 1).trim())); + if (mapKey != null) { + // headers: with foo: bar under it is the entry foo of the headers map option + key = mapKey + "." + key; + } else if (val.isEmpty() && k + 1 < lines.length + && countLeadingSpaces(lines[k + 1]) > paramIndent) { + mapKey = key; + mapIndent = paramIndent; + continue; + } + if (YamlLines.isBlockScalarIndicator(val)) { + // the option is checked by name; its value is the block that follows + blockScalarIndent = paramIndent; + val = ""; + } char sep = hasParams ? '&' : '?'; uriBuilder.append(sep).append(key).append('=').append(val); hasParams = true; 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 15d785f8aebf..23fd298f82d2 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 @@ -40,7 +40,7 @@ final class HeaderChecks { } static final Pattern CAMEL_HEADER_REF_PATTERN = Pattern.compile( - "(?:\\$\\{headers?\\.|headers\\.|headers\\[['\"]|header\\(['\"]|name:\\s*['\"]?)(Camel[A-Z][A-Za-z0-9]*)"); + "(?:\\$\\{headers?\\.|headers\\.|headers\\[['\"]|header\\(['\"]|(name):\\s*['\"]?)(Camel[A-Z][A-Za-z0-9]*(?:\\.[A-Za-z0-9_]+)*)"); static final Pattern SCHEME_IN_URI_PATTERN = Pattern.compile("uri:\\s*\"?([a-zA-Z][a-zA-Z0-9+.-]*):"); @@ -100,8 +100,12 @@ final class HeaderChecks { for (int i = 0; i < lines.length; i++) { Matcher m = CAMEL_HEADER_REF_PATTERN.matcher(lines[i]); while (m.find()) { - String name = m.group(1); - if (common.contains(name) || reported.contains(name)) { + if (m.group(1) != null && !isHeaderName(lines, i)) { + // name: of a setProperty, setVariable, bean...: not a header + continue; + } + String name = m.group(2); + if (common.contains(name) || reported.contains(name) || isKnown(name, known)) { continue; } String propertyOwner = null; @@ -117,7 +121,7 @@ final class HeaderChecks { + ", not a header (the header would be null): write ${exchangeProperty." + name + "}"); continue; } - if (known.contains(name) || !reported.add(name)) { + if (!reported.add(name)) { continue; } String best = closestName(name, new ArrayList<>(known)); @@ -142,4 +146,46 @@ final class HeaderChecks { return msgs; } + /** + * A header the metadata lists: as is (CamelBox.fileName), by a prefix the component documents with a trailing dot + * (CamelSolrField. for CamelSolrField.id), or as the head of an OGNL path (CamelFileName.length()). + */ + static boolean isKnown(String name, Set<String> known) { + String candidate = name; + while (true) { + if (known.contains(candidate)) { + return true; + } + for (String k : known) { + if (k.endsWith(".") && candidate.startsWith(k)) { + return true; + } + } + int dot = candidate.lastIndexOf('.'); + if (dot < 0) { + return false; + } + candidate = candidate.substring(0, dot); + } + } + + /** Whether the name: on this line belongs to a setHeader or removeHeader step, not a setProperty, a bean... */ + static boolean isHeaderName(String[] lines, int index) { + int indent = YamlLines.countLeadingSpaces(lines[index]); + for (int j = index - 1; j >= 0; j--) { + String line = lines[j]; + if (line.isBlank() || line.trim().startsWith("#")) { + continue; + } + if (YamlLines.countLeadingSpaces(line) < indent) { + String step = line.trim(); + if (step.startsWith("- ")) { + step = step.substring(2).trim(); + } + return step.startsWith("setHeader:") || step.startsWith("removeHeader:"); + } + } + return false; + } + } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLines.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLines.java index 0220b2ee7b86..0f245b07c3d7 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLines.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlLines.java @@ -130,6 +130,33 @@ final class YamlLines { return unquote(trimmed.substring(prefix.length()).trim()); } + /** + * A scalar without the YAML comment that may follow it: {@code 30000 # the default is 300000} is 30000, and a + * quoted scalar keeps a # inside its quotes. + */ + static String stripComment(String val) { + if (val.isEmpty()) { + return val; + } + char quote = val.charAt(0); + if (quote == '"' || quote == '\'') { + int close = val.indexOf(quote, 1); + return close > 0 ? val.substring(0, close + 1) : val; + } + for (int i = 1; i < val.length(); i++) { + if (val.charAt(i) == '#' && Character.isWhitespace(val.charAt(i - 1))) { + return val.substring(0, i).stripTrailing(); + } + } + return val; + } + + /** Whether a value is the indicator of a block scalar: |, >, and the variants with a chomping or indent hint. */ + static boolean isBlockScalarIndicator(String val) { + return !val.isEmpty() && (val.charAt(0) == '|' || val.charAt(0) == '>') && val.length() <= 3 + && val.substring(1).chars().allMatch(c -> c == '-' || c == '+' || Character.isDigit(c)); + } + static String unquote(String val) { if (val.length() >= 2 && val.startsWith("\"") && val.endsWith("\"")) { return val.substring(1, val.length() - 1); diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExamplesTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExamplesTest.java new file mode 100644 index 000000000000..609a2682d77b --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocExamplesTest.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.dsl.jbang.core.commands.ai; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.camel.catalog.CamelCatalog; +import org.apache.camel.catalog.DefaultCamelCatalog; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * CAMEL-24710: every YAML route example of the documentation passes the checks {@code camel validate yaml} and the + * authoring tools run after the schema: endpoint URIs and their options, Simple expressions and Camel headers against + * the catalog. The schema itself is guarded by EipDocExamplesTest in camel-yaml-dsl-validator. + */ +class CatalogDocExamplesTest { + + private static final Pattern YAML_BLOCK = Pattern.compile("\\[source,yaml\\]\\n-{4}\\n(.*?)\\n-{4}", Pattern.DOTALL); + + /** + * Pages whose examples are right for the runtime but fail the catalog because the component metadata disagrees with + * the component (CAMEL-24748 and the notes on it); checked again when the metadata is fixed. + */ + private static final Map<String, String> PAGES_SKIPPED = Map.ofEntries( + Map.entry("azure-storage-datalake-component", "CAMEL-24748: the operation enum lists 2 of 15 values"), + Map.entry("google-secret-manager-component", "CAMEL-24748: the operation enum lists 1 of 4 values"), + Map.entry("google-storage-component", "CAMEL-24748: objectName is labelled producer, the consumer uses it"), + Map.entry("web3j-component", "CAMEL-24748: operation is labelled producer, the consumer uses it"), + Map.entry("grpc-component", "CAMEL-24748: forwardOnError and forwardOnCompleted are labelled consumer"), + Map.entry("netty-http-component", "CAMEL-24748: bootstrapConfiguration is not in the metadata"), + Map.entry("jt400-component", "CAMEL-24748: the output options have no @UriParam; the syntax has literal" + + " segments (QSYS.LIB, objectPath.type) the catalog reads as options"), + Map.entry("olingo2-component", "CAMEL-24748: the syntax is apiName/methodName, the runtime reads" + + " methodName/resourcePath with the api name implicit"), + Map.entry("olingo4-component", "CAMEL-24748: the syntax is apiName/methodName, the runtime reads" + + " methodName/resourcePath with the api name implicit"), + Map.entry("xmpp-component", "CAMEL-24748: port is required in the metadata, the runtime defaults it and" + + " takes a user@ prefix on the host"), + Map.entry("huggingface-component", "CAMEL-24748: task is an enum in the metadata, the runtime takes any" + + " name with predictorBean"), + Map.entry("salesforce-rest-api", "CAMEL-24748: approval.* has no @UriParam")); + + /** Examples that show what only the runtime knows, by page and a text found in the example. */ + private static final Map<String, String> EXAMPLES_SKIPPED = Map.of( + "simple-advanced", "${foo", // a custom simple function registered at runtime + "yaml-dsl", "myStep:"); // a step contributed by a custom YAML deserializer + + private static CamelCatalog catalog; + + private record DocExamples(int examples, List<String> failures) { + } + + @BeforeAll + static void setup() { + catalog = new DefaultCamelCatalog(); + } + + @Test + void everyYamlExampleOfTheDocumentationPassesTheCatalogChecks() { + List<String> pages = new ArrayList<>(); + for (String name : catalog.findDocNames()) { + if (!PAGES_SKIPPED.containsKey(name)) { + pages.add(name); + } + } + DocExamples result = validate(pages); + assertThat(result.examples()).as("YAML route examples found in the documentation").isGreaterThan(1700); + assertThat(result.failures()).as("Documentation examples that fail the catalog checks").isEmpty(); + } + + @Test + void theSkippedPagesStillExist() { + for (String page : PAGES_SKIPPED.keySet()) { + assertThat(catalog.asciiDoc(page)).as("the skipped page %s (drop the entry when it is gone)", page).isNotNull(); + } + } + + private static DocExamples validate(List<String> pages) { + int examples = 0; + List<String> failures = new ArrayList<>(); + for (String page : pages) { + String doc = catalog.asciiDoc(page); + if (doc == null) { + continue; + } + int n = 0; + Matcher m = YAML_BLOCK.matcher(doc); + while (m.find()) { + String yaml = m.group(1).stripTrailing() + "\n"; + if (!yaml.stripLeading().startsWith("- ")) { + // a fragment (an option list, a snippet), not a route file + continue; + } + n++; + String skipped = EXAMPLES_SKIPPED.get(page); + if (skipped != null && yaml.contains(skipped)) { + continue; + } + examples++; + for (String msg : SourceValidator.validateCamelYaml(yaml, catalog)) { + failures.add(page + " example " + n + ": " + msg); + } + } + } + return new DocExamples(examples, failures); + } +} 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 911d3a3c7fa3..a9694ee8908b 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 @@ -398,6 +398,64 @@ class SourceValidatorEndpointTest { .contains("${exchangeProperty.CamelTimerCounter}"); } + @Test + void aPropertyOrBeanNameIsNotAHeader() { + // CAMEL-24710: name: counts as a header only under setHeader/removeHeader; a dotted name is looked up as is, + // by the prefix the component documents (CamelSolrField.), and as the head of an OGNL path + List<String> msgs = SourceValidator.validateKnownHeaders(""" + - beans: + - name: CamelMyBean + type: "#class:com.example.MyBean" + - from: + uri: "file:in?noop=true" + steps: + - setProperty: + name: CamelAwsSqsDeleteFiltered + constant: "true" + - setHeader: + name: CamelSolrField.id + simple: "${body}" + - log: "${header.CamelFileName.length()} ${header.CamelFileNam.length()}" + - to: + uri: solr:localhost:8983/mycollection + """, catalog); + assertThat(msgs).hasSize(1); + assertThat(msgs.get(0)).contains("header CamelFileNam.length is not set by").contains("did you mean CamelFileLength"); + } + + @Test + void aCommentABlockScalarAndANestedMapInTheParametersAreRead() { + // CAMEL-24710: a comment after a value is not the value; the body of a block scalar is not options; the keys + // under a Map option are its entries + List<String> msgs = SourceValidator.validateYamlEndpoints(""" + - from: + uri: ai-tool:createOrder + parameters: + description: "Create an order" + argSchema: | + { + "type": "object", + "properties": { "id": { "type": "string" } } + } + steps: + - to: + uri: log:out + parameters: + groupSize: 30000 # the default is none + showAll: true + - to: + uri: once + parameters: + name: hello + headers: + foo: foolish + bar: 456 + unknownOne: x + """, catalog); + assertThat(msgs).hasSize(1); + assertThat(msgs.get(0)).contains("Unknown option 'unknownOne'"); + } + @Test void aProducerOnlyComponentInFromIsNamed() { List<String> msgs = SourceValidator.validateYamlEndpoints(""" diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/EipDocExamplesTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/EipDocExamplesTest.java index 505ec1386e95..248ea4363421 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/EipDocExamplesTest.java +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/EipDocExamplesTest.java @@ -19,7 +19,6 @@ package org.apache.camel.dsl.yaml.validator; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -48,13 +47,6 @@ class EipDocExamplesTest { "point-to-point-channel", "publish-subscribe-channel", "return-address", "scatter-gather", "selective-consumer", "service-activator", "transactional-client"); - /** - * The xmlSecurity examples write the cipher algorithms as the URIs the data format hands to XMLCipher, while the - * model's enums list the XMLCipher constant names, which XMLCipher rejects (CAMEL-24716). The page is checked again - * once the model accepts what the data format does. - */ - private static final Set<String> DATA_FORMAT_PAGES_SKIPPED = Set.of("xmlSecurity-dataformat"); - private static final Pattern YAML_BLOCK = Pattern.compile("\\[source,yaml\\]\\n-{4}\\n(.*?)\\n-{4}", Pattern.DOTALL); /** @@ -100,8 +92,7 @@ class EipDocExamplesTest { @Test void everyYamlExampleOfTheDataFormatDocumentationValidates() throws Exception { - List<String> pages - = docNames(name -> name.endsWith("-dataformat") && !DATA_FORMAT_PAGES_SKIPPED.contains(name)); + List<String> pages = docNames(name -> name.endsWith("-dataformat")); DocExamples result = validate(pages);
