You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by "ramu11 (via GitHub)" <gi...@apache.org> on 2026/10/01 05:14:58 UTC
[PR] CAMEL-22813: Add OData component [camel]
ramu11 opened a new pull request, #27168:
URL: https://github.com/apache/camel/pull/27168
## Description
Adds a new producer-only `camel-odata` component for interacting with OData services.
The component provides OData-specific request and response handling while using Camel HTTP as the underlying HTTP transport.
### Supported functionality
* OData CRUD operations:
* `READ_SET`
* `READ_ENTRY`
* `CREATE`
* `UPDATE`
* `DELETE`
* Dynamic entity keys, including string and composite keys
* OData query options:
* `$filter`
* `$select`
* `$expand`
* `$orderby`
* `$top`
* `$skip`
* `$count`
* Endpoint configuration with message-header overrides
* Basic and Bearer authentication
* Message-level `Authorization` header precedence
* SSL context configuration
* ETag / `If-Match` support
* OData response metadata such as `@odata.count` and `@odata.nextLink`
* HTTP response status handling
* JSON request/response handling
`$batch` support is intentionally not included in this initial implementation.
### Testing and validation
The component has dedicated component, endpoint, and producer tests covering CRUD operations, query options, authentication, ETags, key handling, URI encoding, error handling, response metadata, and concurrent endpoint isolation.
Validation performed:
* `./mvnw -pl components/camel-odata -am clean verify`
* `./mvnw -pl components/camel-odata -am clean install -Psourcecheck`
* `git diff --cached --check`
All completed successfully.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4163522989
##########
components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java:
##########
@@ -0,0 +1,1143 @@
+/*
+ * 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.odata;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.http.base.HttpOperationFailedException;
+import org.apache.camel.support.jsse.KeyStoreParameters;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.support.jsse.TrustManagersParameters;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.containing;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.matching;
+import static com.github.tomakehurst.wiremock.client.WireMock.patch;
+import static com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ODataProducerTest extends CamelTestSupport {
+
+ @RegisterExtension
+ static WireMockExtension wireMock = WireMockExtension.newInstance()
+ .options(wireMockConfig()
+ .dynamicPort()
+ .dynamicHttpsPort()
+ .keystorePath(Objects.requireNonNull(ODataProducerTest.class.getResource("/localhost.p12")).getFile())
+ .keystoreType("PKCS12")
+ .keystorePassword("changeit")
+ .keyManagerPassword("changeit"))
+ .build();
+
+ private ODataEndpoint endpoint;
+ private Producer producer;
+
+ @BeforeEach
+ public void setUpProducer() throws Exception {
+ String uri = "odata:http://localhost:" + wireMock.getPort() + "/odata/Products";
+ endpoint = context.getEndpoint(uri, ODataEndpoint.class);
+ producer = endpoint.createProducer();
+ producer.start();
+ }
+
+ @AfterEach
+ public void tearDownProducer() throws Exception {
+ if (producer != null) {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessGetCollection() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[{\"ID\":101,\"Name\":\"Laptop\"},{\"ID\":102,\"Name\":\"Phone\"}]}")));
+
+ Exchange exchange = endpoint.createExchange();
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertTrue(body.containsKey("value"));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products")));
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithKeyHeaderDynamicUri() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":101,\"Name\":\"Laptop\"}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "101");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertEquals("Laptop", body.get("Name"));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithKeyInUri() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":101,\"Name\":\"Laptop\"}")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertEquals("Laptop", body.get("Name"));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessQueryOptionsHeaders() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*Laptop.*"))
+ .withQueryParam("$select", matching(".*ID.*Name.*"))
+ .withQueryParam("$expand", equalTo("Category"))
+ .withQueryParam("$orderby", matching(".*Name.*asc.*"))
+ .withQueryParam("$top", equalTo("5"))
+ .withQueryParam("$skip", equalTo("10"))
+ .withQueryParam("$count", equalTo("true"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"@odata.count\":1,\"value\":[{\"ID\":101,\"Name\":\"Laptop\"}]}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.FILTER, "Name eq 'Laptop'");
+ exchange.getMessage().setHeader(ODataConstants.SELECT, "ID,Name");
+ exchange.getMessage().setHeader(ODataConstants.EXPAND, "Category");
+ exchange.getMessage().setHeader(ODataConstants.ORDER_BY, "Name asc");
+ exchange.getMessage().setHeader(ODataConstants.TOP, 5);
+ exchange.getMessage().setHeader(ODataConstants.SKIP, 10);
+ exchange.getMessage().setHeader(ODataConstants.INCLUDE_COUNT, true);
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals(1L, exchange.getMessage().getHeader(ODataConstants.COUNT));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*Laptop.*"))
+ .withQueryParam("$select", matching(".*ID.*Name.*"))
+ .withQueryParam("$expand", equalTo("Category"))
+ .withQueryParam("$orderby", matching(".*Name.*asc.*"))
+ .withQueryParam("$top", equalTo("5"))
+ .withQueryParam("$skip", equalTo("10"))
+ .withQueryParam("$count", equalTo("true")));
+ }
+
+ @Test
+ void testProcessCreateEntity() throws Exception {
+ wireMock.stubFor(post(urlPathEqualTo("/odata/Products"))
+ .withRequestBody(equalToJson("{\"Name\":\"Tablet\",\"Price\":299.99}"))
+ .willReturn(aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":103,\"Name\":\"Tablet\",\"Price\":299.99}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.CREATE);
+
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("Name", "Tablet");
+ payload.put("Price", 299.99);
+ exchange.getMessage().setBody(payload);
+
+ producer.process(exchange);
+
+ assertEquals(201, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertEquals(new BigDecimal(103), body.get("ID"));
+ }
+
+ @Test
+ void testProcessIgnoresInboundHttpRoutingHeaders() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange = endpoint.createExchange();
+
+ // Simulate HTTP/REST headers inherited from an upstream consumer.
+ exchange.getMessage().setHeader(Exchange.HTTP_URI, "http://malicious.example/other");
+ exchange.getMessage().setHeader(Exchange.HTTP_PATH, "/wrong/path");
+ exchange.getMessage().setHeader(Exchange.HTTP_QUERY, "unexpected=true");
+ exchange.getMessage().setHeader(Exchange.HTTP_RAW_QUERY, "rawUnexpected=true");
+ exchange.getMessage().setHeader("CamelRestHttpUri", "http://malicious.example/rest");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products")));
+ }
+
+ @Test
+ void testProcessUpdateEntity() throws Exception {
+ ODataEndpoint keyEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer keyProducer = keyEndpoint.createProducer();
+ keyProducer.start();
+
+ try {
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withRequestBody(equalToJson("{\"Name\":\"Updated Laptop\"}"))
+ .willReturn(aResponse().withStatus(204)));
+
+ Exchange exchange = keyEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.UPDATE);
+
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("Name", "Updated Laptop");
+ exchange.getMessage().setBody(payload);
+
+ keyProducer.process(exchange);
+
+ assertEquals(204, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ } finally {
+ keyProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessDeleteEntity() throws Exception {
+ ODataEndpoint keyEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer keyProducer = keyEndpoint.createProducer();
+ keyProducer.start();
+
+ try {
+ wireMock.stubFor(delete(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse().withStatus(204)));
+
+ Exchange exchange = keyEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.DELETE);
+
+ keyProducer.process(exchange);
+
+ assertEquals(204, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNull(exchange.getMessage().getBody());
+ } finally {
+ keyProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithETagHeader() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("\"etag-v1\""))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":101,\"Name\":\"Laptop\",\"@odata.etag\":\"etag-v2\"}")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.ETAG, "\"etag-v1\"");
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("etag-v2", exchange.getMessage().getHeader(ODataConstants.ETAG));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithHttpETag() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(102)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(102)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withHeader("ETag", "W/\"etag-v3\"")
+ .withBody("{\"ID\":102,\"Name\":\"Monitor\"}")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("W/\"etag-v3--gzip\"", exchange.getMessage().getHeader(ODataConstants.ETAG));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessUpdateEntityWithETag() throws Exception {
+ ODataEndpoint updateEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer updateProducer = updateEndpoint.createProducer();
+ updateProducer.start();
+
+ try {
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"etag-v3\""))
+ .withHeader("Content-Type", containing("application/json"))
+ .withRequestBody(equalToJson("{\"Name\":\"Updated Laptop\"}"))
+ .willReturn(aResponse()
+ .withStatus(204)
+ .withHeader("ETag", "W/\"etag-v4\"")));
+
+ Exchange exchange = updateEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.UPDATE);
+ exchange.getMessage().setHeader(ODataConstants.ETAG, "W/\"etag-v3\"");
+ exchange.getMessage().setBody("{\"Name\":\"Updated Laptop\"}");
+
+ updateProducer.process(exchange);
+
+ assertEquals(204, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("W/\"etag-v4\"", exchange.getMessage().getHeader(ODataConstants.ETAG));
+
+ wireMock.verify(patchRequestedFor(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"etag-v3\"")));
+ } finally {
+ updateProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessUpdateEntityWithETagPreconditionFailed() throws Exception {
+ ODataEndpoint updateEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer updateProducer = updateEndpoint.createProducer();
+ updateProducer.start();
+
+ try {
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"stale-etag\""))
+ .willReturn(aResponse()
+ .withStatus(412)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "error": {
+ "code": "412",
+ "message": "The ETag does not match."
+ }
+ }
+ """)));
+
+ Exchange exchange = updateEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.UPDATE);
+ exchange.getMessage().setHeader(ODataConstants.ETAG, "W/\"stale-etag\"");
+ exchange.getMessage().setBody("{\"Name\":\"Updated Laptop\"}");
+
+ HttpOperationFailedException exception = assertThrows(
+ HttpOperationFailedException.class,
+ () -> updateProducer.process(exchange));
+
+ assertEquals(412, exception.getStatusCode());
+ wireMock.verify(patchRequestedFor(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"stale-etag\"")));
+ } finally {
+ updateProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadSetWithEncodedFilter() throws Exception {
+ ODataEndpoint readEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer readProducer = readEndpoint.createProducer();
+ readProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo("Name eq 'Laptop Pro'"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "value": [
+ {"ID": 101, "Name": "Laptop Pro"}
+ ]
+ }
+ """)));
+
+ Exchange exchange = readEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_SET);
+ exchange.getMessage().setHeader(ODataConstants.FILTER, "Name eq 'Laptop Pro'");
+
+ readProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo("Name eq 'Laptop Pro'")));
+ } finally {
+ readProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadSetWithComplexEncodedFilter() throws Exception {
+ ODataEndpoint readEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer readProducer = readEndpoint.createProducer();
+ readProducer.start();
+
+ String filter = "Name eq 'O''Reilly' and Category eq 'Office/Supplies' "
+ + "and Price gt 10.5 and (Status eq 'Active' or Code eq 'A%20B') "
+ + "and startswith(Name,'Pro')";
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo(filter))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "value": [
+ {
+ "ID": 101,
+ "Name": "O'Reilly Pro",
+ "Category": "Office/Supplies"
+ }
+ ]
+ }
+ """)));
+
+ Exchange exchange = readEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_SET);
+ exchange.getMessage().setHeader(ODataConstants.FILTER, filter);
+
+ readProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo(filter)));
+ } finally {
+ readProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadEntryWithTrailingSlashAndKey() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products/",
+ ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "ID": 101,
+ "Name": "Laptop"
+ }
+ """)));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "101");
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessNextLinkHeader() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[],\"@odata.nextLink\":\"http://localhost/odata/Products?$skip=10\"}")));
+
+ Exchange exchange = endpoint.createExchange();
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("http://localhost/odata/Products?$skip=10", exchange.getMessage().getHeader(ODataConstants.NEXT_LINK));
+ }
+
+ @Test
+ void testProcessEndpointConfigurationDefaults() throws Exception {
+ ODataEndpoint configuredEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?top=5&skip=10&filter=Name eq 'Phone'",
+ ODataEndpoint.class);
+ Producer configuredProducer = configuredEndpoint.createProducer();
+ configuredProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$top", equalTo("5"))
+ .withQueryParam("$skip", equalTo("10"))
+ .withQueryParam("$filter", matching(".*Name.*Phone.*"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[{\"ID\":102,\"Name\":\"Phone\"}]}")));
+
+ Exchange exchange = configuredEndpoint.createExchange();
+ configuredProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ } finally {
+ configuredProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessHeaderOverridesEndpointConfiguration() throws Exception {
+ ODataEndpoint configuredEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?top=5",
+ ODataEndpoint.class);
+ Producer configuredProducer = configuredEndpoint.createProducer();
+ configuredProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$top", equalTo("20"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange = configuredEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.TOP, 20);
+
+ configuredProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ } finally {
+ configuredProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessHttpErrorPropagatesException() {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse().withStatus(500)));
+
+ Exchange exchange = endpoint.createExchange();
+
+ assertThrows(HttpOperationFailedException.class, () -> producer.process(exchange));
+ }
+
+ @Test
+ void testProcessEmptyResponseBodyReturnsNullBody() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNull(exchange.getMessage().getBody());
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProducerSingletonAndLifecycle() {
+ assertTrue(producer.isSingleton());
+ }
+
+ @Test
+ void testBasicAuthentication() throws Exception {
+ String username = "adminUser";
+ String password = "adminPassword123";
+ String expectedHeader
+ = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
+
+ ODataEndpoint authEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authMethod=Basic&authUsername=" + username
+ + "&authPassword=" + password,
+ ODataEndpoint.class);
+ Producer authProducer = authEndpoint.createProducer();
+ authProducer.start();
+
+ try {
+ // Priority 1: Match and return 200 OK when correct credentials are present
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(1)
+ .withHeader("Authorization", equalTo(expectedHeader))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ // Priority 10: Fallback HTTP Basic 401 challenge with WWW-Authenticate header
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(10)
+ .willReturn(aResponse()
+ .withStatus(401)
+ .withHeader("WWW-Authenticate", "Basic realm=\"OData Test Realm\"")));
+
+ Exchange exchange = authEndpoint.createExchange();
+ authProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(expectedHeader)));
+ } finally {
+ authProducer.stop();
+ }
+ }
+
+ @Test
+ void testBearerAuthenticationAndExchangeHeaderPrecedence() throws Exception {
+ String configuredToken = "configuredBearerToken123";
+ String explicitHeaderToken = "Bearer explicitHeaderToken456";
+
+ ODataEndpoint authEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authBearerToken=" + configuredToken,
+ ODataEndpoint.class);
+ Producer authProducer = authEndpoint.createProducer();
+ authProducer.start();
+
+ try {
+ // Case 1: Configured bearer token attached automatically
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + configuredToken))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange1 = authEndpoint.createExchange();
+ authProducer.process(exchange1);
+ assertEquals(200, exchange1.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + configuredToken)));
+
+ // Case 2: Explicit Exchange Authorization header takes precedence over configured token
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(explicitHeaderToken))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange2 = authEndpoint.createExchange();
+ exchange2.getMessage().setHeader("Authorization", explicitHeaderToken);
+ authProducer.process(exchange2);
+ assertEquals(200, exchange2.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(explicitHeaderToken)));
+ } finally {
+ authProducer.stop();
+ }
+ }
+
+ @Test
+ void testBearerEndpointConcurrentIsolation() throws Exception {
+ String token1 = "tokenUnit1";
+ String token2 = "tokenUnit2";
+
+ ODataEndpoint endpoint1 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?authBearerToken=" + token1,
+ ODataEndpoint.class);
+ ODataEndpoint endpoint2 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?authBearerToken=" + token2,
+ ODataEndpoint.class);
+
+ Producer producer1 = endpoint1.createProducer();
+ Producer producer2 = endpoint2.createProducer();
+
+ producer1.start();
+ producer2.start();
+
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ int threadCount = 10;
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ ConcurrentLinkedQueue<Throwable> failures = new ConcurrentLinkedQueue<>();
+
+ try {
+ for (int i = 0; i < threadCount; i++) {
+ final int index = i;
+ executor.submit(() -> {
+ Exchange exchange = null;
+ try {
+ Producer targetProducer = (index % 2 == 0) ? producer1 : producer2;
+ ODataEndpoint targetEndpoint = (index % 2 == 0) ? endpoint1 : endpoint2;
+ exchange = targetEndpoint.createExchange();
+ targetProducer.process(exchange);
+ if (exchange.getException() != null) {
+ failures.add(exchange.getException());
+ }
+ } catch (Throwable t) {
+ failures.add(t);
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ assertTrue(failures.isEmpty(), "Worker threads encountered exceptions: " + failures);
+
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + token1)));
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + token2)));
+ } finally {
+ executor.shutdownNow();
+ producer1.stop();
+ producer2.stop();
+ }
+ }
+
+ @Test
+ void testBasicAuthConcurrentIsolation() throws Exception {
+ String user1 = "user1";
+ String pass1 = "pass1";
+ String expectedHeader1
+ = "Basic " + Base64.getEncoder().encodeToString((user1 + ":" + pass1).getBytes(StandardCharsets.UTF_8));
+
+ String user2 = "user2";
+ String pass2 = "pass2";
+ String expectedHeader2
+ = "Basic " + Base64.getEncoder().encodeToString((user2 + ":" + pass2).getBytes(StandardCharsets.UTF_8));
+
+ ODataEndpoint endpoint1 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authMethod=Basic&authUsername=" + user1
+ + "&authPassword=" + pass1,
+ ODataEndpoint.class);
+ ODataEndpoint endpoint2 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authMethod=Basic&authUsername=" + user2
+ + "&authPassword=" + pass2,
+ ODataEndpoint.class);
+
+ Producer producer1 = endpoint1.createProducer();
+ Producer producer2 = endpoint2.createProducer();
+
+ producer1.start();
+ producer2.start();
+
+ int threadCount = 10;
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+
+ try {
+ // Priority 1: Match and return 200 OK when user1 credentials are present
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(1)
+ .withHeader("Authorization", equalTo(expectedHeader1))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ // Priority 1: Match and return 200 OK when user2 credentials are present
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(1)
+ .withHeader("Authorization", equalTo(expectedHeader2))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ // Priority 10: Fallback HTTP Basic 401 challenge with WWW-Authenticate header
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(10)
+ .willReturn(aResponse()
+ .withStatus(401)
+ .withHeader("WWW-Authenticate", "Basic realm=\"OData Test Realm\"")));
+
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ ConcurrentLinkedQueue<Throwable> failures = new ConcurrentLinkedQueue<>();
+
+ for (int i = 0; i < threadCount; i++) {
+ final int index = i;
+ executor.submit(() -> {
+ Exchange exchange = null;
+ try {
+ Producer targetProducer = (index % 2 == 0) ? producer1 : producer2;
+ ODataEndpoint targetEndpoint = (index % 2 == 0) ? endpoint1 : endpoint2;
+ exchange = targetEndpoint.createExchange();
+ targetProducer.process(exchange);
+ if (exchange.getException() != null) {
+ failures.add(exchange.getException());
+ }
+ } catch (Throwable t) {
+ failures.add(t);
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ assertTrue(failures.isEmpty(), "Worker threads encountered exceptions: " + failures);
+
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(expectedHeader1)));
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(expectedHeader2)));
+ } finally {
+ executor.shutdownNow();
+ producer1.stop();
+ producer2.stop();
+ }
+ }
+
+ @Test
+ void testQueryEncodingWithAmpersand() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*John.*Sons.*"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.FILTER, "Name eq 'John & Sons'");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*John.*Sons.*")));
+ }
+
+ @Test
+ void testProcessReadEntryWithStringKey() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer producer = endpoint.createProducer();
+ producer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products('ABC-123')"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {"ID":"ABC-123","Name":"Laptop"}
+ """)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "'ABC-123'");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products('ABC-123')")));
+ } finally {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadEntryWithCompositeKey() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer producer = endpoint.createProducer();
+ producer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo(
+ "/odata/Products(ProductID=101,CategoryID=5)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {"ProductID":101,"CategoryID":5,"Name":"Laptop"}
+ """)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(
+ ODataConstants.KEY, "ProductID=101,CategoryID=5");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo(
+ "/odata/Products(ProductID=101,CategoryID=5)")));
+ } finally {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessWithConfiguredBasicAuthentication() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+
+ endpoint.getConfiguration().setAuthMethod("Basic");
+ endpoint.getConfiguration().setAuthUsername("odata-user");
+ endpoint.getConfiguration().setAuthPassword("odata-password");
+
+ Producer producer = endpoint.createProducer();
+ producer.start();
+
+ try {
+ String credentials = Base64.getEncoder()
+ .encodeToString("odata-user:odata-password".getBytes(StandardCharsets.UTF_8));
+
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Basic " + credentials))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "value": []
+ }
+ """)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_SET);
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Basic " + credentials)));
+ } finally {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessHttpsWithSslContextParameters() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ KeyStoreParameters trustStore = new KeyStoreParameters();
+ trustStore.setResource("localhost.p12");
+ trustStore.setPassword("changeit");
+ trustStore.setType("PKCS12");
+
+ TrustManagersParameters trustManagers = new TrustManagersParameters();
+ trustManagers.setKeyStore(trustStore);
+
+ SSLContextParameters sslContextParameters = new SSLContextParameters();
+ sslContextParameters.setTrustManagers(trustManagers);
+
+ String baseUri = "odata:https://localhost:" + wireMock.getHttpsPort() + "/odata/Products";
+
+ // Use different OData endpoint URIs while keeping the underlying HTTP URI identical.
+ ODataEndpoint httpsEndpoint = context.getEndpoint(
+ baseUri + "?useGlobalSslContextParameters=true", ODataEndpoint.class);
+ httpsEndpoint.setSslContextParameters(sslContextParameters);
+
+ ODataEndpoint endpointWithoutSsl = context.getEndpoint(
+ baseUri + "?useGlobalSslContextParameters=false", ODataEndpoint.class);
+
+ Producer httpsProducer = httpsEndpoint.createProducer();
+ Producer endpointWithoutSslProducer = endpointWithoutSsl.createProducer();
+
+ try {
+ httpsProducer.start();
+
+ Exchange exchange = httpsEndpoint.createExchange();
+ httpsProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNotNull(exchange.getMessage().getBody());
+
+ endpointWithoutSslProducer.start();
+
+ Exchange exchangeWithoutSsl = endpointWithoutSsl.createExchange();
+
+ Throwable thrown = assertThrows(Throwable.class, () -> endpointWithoutSslProducer.process(exchangeWithoutSsl));
+
+ // Check for SSLException / SSLHandshakeException in the cause chain
+ boolean hasSslCause = false;
+ Throwable current = thrown;
+ while (current != null) {
+ if (current instanceof javax.net.ssl.SSLException) {
+ hasSslCause = true;
+ break;
+ }
+ current = current.getCause();
+ }
+
+ assertTrue(hasSslCause, "Expected SSL failure cause, but received: " + thrown);
Review Comment:
Thanks! Addressed. I replaced the manual cause-chain traversal with `ObjectHelper.getException(...)` and added the `SSLException` and `ObjectHelper` imports as suggested. The OData tests pass successfully.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4163525569
##########
components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java:
##########
@@ -0,0 +1,1143 @@
+/*
+ * 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.odata;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.http.base.HttpOperationFailedException;
+import org.apache.camel.support.jsse.KeyStoreParameters;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.support.jsse.TrustManagersParameters;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.containing;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.matching;
+import static com.github.tomakehurst.wiremock.client.WireMock.patch;
+import static com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ODataProducerTest extends CamelTestSupport {
+
+ @RegisterExtension
+ static WireMockExtension wireMock = WireMockExtension.newInstance()
+ .options(wireMockConfig()
+ .dynamicPort()
+ .dynamicHttpsPort()
+ .keystorePath(Objects.requireNonNull(ODataProducerTest.class.getResource("/localhost.p12")).getFile())
Review Comment:
Thanks! Addressed. I replaced `URL.getFile()` with `Paths.get(url.toURI()).toString()` via a small static helper to handle URL-encoded paths correctly. The OData tests pass successfully.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus merged PR #27168:
URL: https://github.com/apache/camel/pull/27168
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #27168:
URL: https://github.com/apache/camel/pull/27168#issuecomment-5925216951
:star2: Thank you for your contribution to the Apache Camel project! :star2:
:robot: CI automation will test this PR automatically.
:camel: Apache Camel Committers, please review the following items:
* First-time contributors **require MANUAL approval** for the GitHub Actions to run
* You can use the command `/component-test (camel-)component-name1 (camel-)component-name2..` to request a test from the test bot although they are normally detected and executed by CI.
* You can label PRs using `skip-tests` and `test-dependents` to fine-tune the checks executed by this PR.
* Build and test logs are available in the summary page. **Only** [Apache Camel committers](https://camel.apache.org/community/team/#committers) have access to the summary.
:warning: Be careful when sharing logs. Review their contents before sharing them publicly.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154144663
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * 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.odata;
+
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriParams;
+import org.apache.camel.support.jsse.SSLContextParameters;
+
+@UriParams
+public class ODataConfiguration {
+
+ @UriParam(label = "producer", description = "The OData operation to perform")
+ private ODataOperation operation = ODataOperation.READ_SET;
+
+ @UriParam(label = "producer", description = "The OData $filter query parameter")
+ private String filter;
+
+ @UriParam(label = "producer", description = "The OData $select query parameter")
+ private String select;
+
+ @UriParam(label = "producer", description = "The OData $expand query parameter")
+ private String expand;
+
+ @UriParam(label = "producer", description = "The OData $orderby query parameter")
+ private String orderBy;
+
+ @UriParam(label = "producer", description = "The OData $top query parameter")
+ private Integer top;
+
+ @UriParam(label = "producer", description = "The OData $skip query parameter")
+ private Integer skip;
+
+ @UriParam(label = "producer", description = "The OData $count query parameter")
+ private Boolean count;
+
+ @UriParam(label = "security", description = "Authentication method to use (e.g., Basic, Bearer)")
+ private String authMethod;
+
+ @UriParam(label = "security", description = "Username for Basic authentication")
+ private String authUsername;
+
+ @UriParam(label = "security", description = "Password for Basic authentication")
+ private String authPassword;
+
+ @UriParam(label = "security", description = "Bearer token for Bearer authentication")
+ private String authBearerToken;
+
+ @UriParam(label = "security", description = "To use a custom SSLContextParameters")
Review Comment:
Addressed in the latest commit `4cb8cea36f16`.
`authBearerToken` is marked as a secret using Camel's current `security = "secret"` annotation, which generates the secret metadata and masks the value. The deprecated `secret = true` attribute was not used.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154130032
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,359 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData (Open Data Protocol) services using standard OData CRUD operations.
+
+The component delegates HTTP transport to Camel HTTP while providing OData-specific operation, key, query option, authentication, and response handling.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+## [source,xml]
Review Comment:
Addressed in the latest commit `4cb8cea36f16`.
* Corrected all `## [source,...]` code-block markers to valid `[source,...]` AsciiDoc syntax.
* Fixed the ETag Java examples by escaping the embedded quotes.
* Ran `git diff --check` and the OData component test suite successfully.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154147956
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * 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.odata;
+
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriParams;
+import org.apache.camel.support.jsse.SSLContextParameters;
+
+@UriParams
+public class ODataConfiguration {
+
+ @UriParam(label = "producer", description = "The OData operation to perform")
+ private ODataOperation operation = ODataOperation.READ_SET;
+
+ @UriParam(label = "producer", description = "The OData $filter query parameter")
+ private String filter;
+
+ @UriParam(label = "producer", description = "The OData $select query parameter")
+ private String select;
+
+ @UriParam(label = "producer", description = "The OData $expand query parameter")
+ private String expand;
+
+ @UriParam(label = "producer", description = "The OData $orderby query parameter")
+ private String orderBy;
+
+ @UriParam(label = "producer", description = "The OData $top query parameter")
+ private Integer top;
+
+ @UriParam(label = "producer", description = "The OData $skip query parameter")
+ private Integer skip;
+
+ @UriParam(label = "producer", description = "The OData $count query parameter")
+ private Boolean count;
+
+ @UriParam(label = "security", description = "Authentication method to use (e.g., Basic, Bearer)")
+ private String authMethod;
+
+ @UriParam(label = "security", description = "Username for Basic authentication")
+ private String authUsername;
+
+ @UriParam(label = "security", description = "Password for Basic authentication")
+ private String authPassword;
Review Comment:
Addressed in the latest commit `4cb8cea36f16`.
`authPassword` is marked as a secret using Camel's current `security = "secret"` annotation. The generated component metadata now marks it as secret, and it is included in `SECRET_PROPERTY_NAMES` for URI sanitization.
The suggested `secret = true` attribute is deprecated, so `security = "secret"` is used instead.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154159176
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * 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.odata;
+
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriParams;
+import org.apache.camel.support.jsse.SSLContextParameters;
+
+@UriParams
+public class ODataConfiguration {
+
+ @UriParam(label = "producer", description = "The OData operation to perform")
+ private ODataOperation operation = ODataOperation.READ_SET;
+
+ @UriParam(label = "producer", description = "The OData $filter query parameter")
+ private String filter;
+
+ @UriParam(label = "producer", description = "The OData $select query parameter")
+ private String select;
+
+ @UriParam(label = "producer", description = "The OData $expand query parameter")
+ private String expand;
+
+ @UriParam(label = "producer", description = "The OData $orderby query parameter")
+ private String orderBy;
+
+ @UriParam(label = "producer", description = "The OData $top query parameter")
+ private Integer top;
+
+ @UriParam(label = "producer", description = "The OData $skip query parameter")
+ private Integer skip;
+
+ @UriParam(label = "producer", description = "The OData $count query parameter")
+ private Boolean count;
+
+ @UriParam(label = "security", description = "Authentication method to use (e.g., Basic, Bearer)")
+ private String authMethod;
+
+ @UriParam(label = "security", description = "Username for Basic authentication")
+ private String authUsername;
+
+ @UriParam(label = "security", description = "Password for Basic authentication")
+ private String authPassword;
Review Comment:
done
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4161569652
##########
components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java:
##########
@@ -0,0 +1,1130 @@
+/*
+ * 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.odata;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import com.google.common.io.Resources;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.http.base.HttpOperationFailedException;
+import org.apache.camel.support.jsse.KeyStoreParameters;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.support.jsse.TrustManagersParameters;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.containing;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.matching;
+import static com.github.tomakehurst.wiremock.client.WireMock.patch;
+import static com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ODataProducerTest extends CamelTestSupport {
+
+ @RegisterExtension
+ static WireMockExtension wireMock = WireMockExtension.newInstance()
+ .options(wireMockConfig()
+ .dynamicPort()
+ .dynamicHttpsPort()
+ .keystorePath(Resources.getResource("localhost.p12").toString())
Review Comment:
addressed in new commit 4ac88cda5073
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4156221226
##########
components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java:
##########
@@ -0,0 +1,1130 @@
+/*
+ * 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.odata;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import com.google.common.io.Resources;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.http.base.HttpOperationFailedException;
+import org.apache.camel.support.jsse.KeyStoreParameters;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.support.jsse.TrustManagersParameters;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.containing;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.matching;
+import static com.github.tomakehurst.wiremock.client.WireMock.patch;
+import static com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ODataProducerTest extends CamelTestSupport {
+
+ @RegisterExtension
+ static WireMockExtension wireMock = WireMockExtension.newInstance()
+ .options(wireMockConfig()
+ .dynamicPort()
+ .dynamicHttpsPort()
+ .keystorePath(Resources.getResource("localhost.p12").toString())
Review Comment:
💡 **Nit (carried forward from davsclaus):** `Resources.getResource("localhost.p12")` pulls in `com.google.common.io.Resources`. Since this class is only used for this one call, prefer `getClass().getResource("/localhost.p12").toString()` to avoid the explicit Guava dependency.
```suggestion
.keystorePath(getClass().getResource("/localhost.p12").toString())
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154140139
##########
catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/odata.json:
##########
@@ -0,0 +1,62 @@
+{
+ "component": {
+ "kind": "component",
+ "name": "odata",
+ "title": "OData",
+ "description": "Camel OData Component",
+ "deprecated": false,
+ "firstVersion": "4.23.0",
Review Comment:
⚠️ **Stale generated file:** This catalog-level JSON was generated from the Regen commit and not updated after the latest commit. It still has:
- `"label": "http,cloud"` (should be `"http"` — `Category.CLOUD` was removed in `ODataEndpoint.java`)
- `"secret": false` on `authPassword` and `authBearerToken` (should be `true` — `security = "secret"` was added in `ODataConfiguration.java`)
The component's own `META-INF/.../odata.json` is correct. A regen pass (`mvn generate-resources` from the repo root, or the Camel `regen` workflow) will sync this.
Also: `catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/others.properties` still lists `odata`, and `catalog/.../others/odata.json` still exists with the stale "other" descriptor. These should be removed by the regen (now that `other.properties` was deleted from the component).
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,406 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData V4 services using standard OData CRUD operations.
+
+The component provides OData-specific request and response handling while using Camel HTTP as the underlying HTTP transport.
+
+The OData component is the successor to the deprecated `camel-olingo4` component.
+
+The component supports OData V4 services using JSON representations.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+[source,xml]
+
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-odata</artifactId>
+ <version>x.x.x</version>
+ <!-- use the same version as your Camel core version -->
+</dependency>
+----
+
+== URI Format
+
+=== odata:httpUri
+
+Where `httpUri` is the base URL of the OData entity set or resource.
+
+For example:
+
+[source,text]
+
+odata:http://localhost:8080/odata/Products
+odata:https://services.odata.org/V4/Northwind/Northwind.svc/Categories
+----------------------------------------------------------------------
+
+The URI can also contain endpoint options as query parameters.
+
+The OData query options can alternatively be configured on the endpoint or supplied dynamically using message headers.
+
+The component uses Camel HTTP for the underlying HTTP transport.
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+# [NOTE]
+
+# This is a producer-only component. Per-message options can be supplied through `CamelOData` headers in addition to endpoint options.
+
+# [NOTE]
+
+# If the route receives messages from an untrusted producer, take care when forwarding internal Camel headers to this component. In particular, an inbound `Authorization` header can be used for the OData request and takes precedence over endpoint authentication.
+
+== Operations
+
+The operation is selected using the `CamelODataOperation` message header or the `operation` endpoint option.
+
+The following operations are supported:
+
+[cols="20%,15%,65%",options="header"]
+|===
+| Operation | HTTP Method | Description
+
+| `READ_SET`
+| `GET`
+| Reads a collection of entities. This is the default operation.
+
+| `READ_ENTRY`
+| `GET`
+| Reads a single entity. Use `CamelODataKey` to specify the entity key.
+
+| `CREATE`
+| `POST`
+| Creates a new entity using the message body as the request payload.
+
+| `UPDATE`
+| `PATCH`
+| Updates an existing entity using `CamelODataKey` to identify the entity.
+
+| `DELETE`
+| `DELETE`
+| Deletes an existing entity using `CamelODataKey` to identify the entity.
+|===
+
+For `READ_ENTRY`, `UPDATE`, and `DELETE`, the key is appended to the configured resource URI.
+
+For example:
+
+[source,text]
+
+CamelODataKey = 101
+odata:http://localhost:8080/odata/Products
+------------------------------------------
+
+results in:
+
+[source,text]
+
+http://localhost:8080/odata/Products(101)
+
+The key is treated as an OData key expression. The component does not automatically add quotes or otherwise change the supplied key value.
+
+For example, a string key can be supplied as `'ABC-123'`, while a composite key can be supplied as `ProductID=101,CategoryID=5`.
+
+The key value should be a valid and trusted OData key expression.
+
+== Message Headers
+
+The following message headers can be used to control OData processing dynamically.
+
+[cols="25%,20%,55%",options="header"]
+|===
+| Header | Type | Description
+
+| `CamelODataOperation`
+| `ODataOperation` / `String`
+| Specifies the operation to execute.
+
+| `CamelODataKey`
+| `String`
+| Specifies the key for `READ_ENTRY`, `UPDATE`, and `DELETE` operations.
+
+| `CamelODataFilter`
+| `String`
+| Overrides the `$filter` query option.
+
+| `CamelODataSelect`
+| `String`
+| Overrides the `$select` query option.
+
+| `CamelODataExpand`
+| `String`
+| Overrides the `$expand` query option.
+
+| `CamelODataOrderBy`
+| `String`
+| Overrides the `$orderby` query option.
+
+| `CamelODataTop`
+| `Integer`
+| Overrides the `$top` query option.
+
+| `CamelODataSkip`
+| `Integer`
+| Overrides the `$skip` query option.
+
+| `CamelODataIncludeCount`
+| `Boolean`
+| Controls whether the `$count` query option is included in the request.
+
+| `CamelODataETag`
+| `String`
+| Supplies an ETag for the `If-Match` HTTP request header. The component also populates this header from an ETag returned by the OData service.
+
+| `CamelODataCount`
+| `Long`
+| Contains the count returned by the OData service when an `@odata.count` value is present.
+
+| `CamelODataNextLink`
+| `String`
+| Contains the `@odata.nextLink` value returned by the OData service when present.
+
+| `CamelHttpResponseCode`
+| `Integer`
+| Contains the HTTP response status code returned by the OData service.
+|===
+
+A message-level `Authorization` header can also be supplied.
+
+When an `Authorization` header is present, it takes precedence over authentication configured on the OData endpoint.
+
+If the OData route is exposed to an upstream HTTP consumer, such as `platform-http`, take care that an inbound `Authorization` header may be forwarded to the OData service.
+
+If the upstream authorization credentials must not be forwarded, remove the header before sending the exchange to the OData component.
+
+For example:
+
+[source,java]
+
+from("platform-http:/products")
+.removeHeader("Authorization")
+.to("odata:http://my-odata-service/odata/Products");
+----------------------------------------------------
+
+== Query Options
+
+The component supports the following OData query options:
+
+[cols="25%,20%,55%",options="header"]
+|===
+| Option | Type | Description
+
+| `filter`
+| `String`
+| OData `$filter` expression.
+
+| `select`
+| `String`
+| OData `$select` expression.
+
+| `expand`
+| `String`
+| OData `$expand` expression.
+
+| `orderBy`
+| `String`
+| OData `$orderby` expression.
+
+| `top`
+| `Integer`
+| OData `$top` value.
+
+| `skip`
+| `Integer`
+| OData `$skip` value.
+
+| `count`
+| `Boolean`
+| Controls the OData `$count` query option.
+|===
+
+For example:
+
+[source,text]
+
+odata:http://localhost:8080/odata/Products?filter=Name%20eq%20%27Laptop%27&select=Name,Price&top=10
+
+Query options configured through message headers take precedence over the corresponding endpoint configuration.
+
+== Authentication
+
+The component supports Basic authentication and Bearer token authentication through endpoint options.
+
+For example, Basic authentication can be configured using:
+
+[source,text]
+
+odata:http://localhost:8080/odata/Products?authMethod=Basic&authUsername=user&authPassword=password
+
+Bearer token authentication can be configured using the `authBearerToken` option.
+
+Authentication can also be supplied dynamically using the message-level `Authorization` header:
+
+[source,java]
+
+from("direct:getProducts")
+.setHeader("Authorization", constant("Bearer my-token"))
+.to("odata:http://my-odata-service/odata/Products");
+----------------------------------------------------
+
+When an `Authorization` header is present on the message, it takes precedence over endpoint authentication.
+
+Take care when forwarding exchanges from an upstream HTTP consumer because the incoming `Authorization` header may be forwarded to the OData service.
+
+== ETags
+
+The component supports optimistic concurrency using OData/HTTP ETags.
+
+An ETag supplied in `CamelODataETag` is sent to the service using the HTTP `If-Match` header.
+
+For example:
+
+[source,java]
+
+from("direct:updateProduct")
+.setHeader("CamelODataOperation", constant("UPDATE"))
+.setHeader("CamelODataKey", constant("101"))
+.setHeader("CamelODataETag", constant("W/"etag-value""))
Review Comment:
⚠️ **Invalid Java (still present):** `constant("W/"etag-value"")` has unescaped quotes — this is not valid Java. The same issue is on line 379.
The correct escaped form would be:
```
.setHeader("CamelODataETag", constant("W/\"etag-value\""))
```
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,406 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData V4 services using standard OData CRUD operations.
+
+The component provides OData-specific request and response handling while using Camel HTTP as the underlying HTTP transport.
+
+The OData component is the successor to the deprecated `camel-olingo4` component.
+
+The component supports OData V4 services using JSON representations.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+[source,xml]
+
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-odata</artifactId>
+ <version>x.x.x</version>
+ <!-- use the same version as your Camel core version -->
+</dependency>
+----
+
+== URI Format
+
+=== odata:httpUri
+
+Where `httpUri` is the base URL of the OData entity set or resource.
+
+For example:
+
+[source,text]
+
+odata:http://localhost:8080/odata/Products
+odata:https://services.odata.org/V4/Northwind/Northwind.svc/Categories
+----------------------------------------------------------------------
+
+The URI can also contain endpoint options as query parameters.
+
+The OData query options can alternatively be configured on the endpoint or supplied dynamically using message headers.
+
+The component uses Camel HTTP for the underlying HTTP transport.
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+# [NOTE]
Review Comment:
⚠️ **AsciiDoc syntax (still broken):** Lines 61–67 use `# [NOTE]` (Markdown heading prefix) instead of AsciiDoc admonition syntax. In AsciiDoc, a NOTE block should be:
```
[NOTE]
====
This is a producer-only component.
====
```
or inline: `NOTE: This is a producer-only component.`
The `#` prefix renders as a literal `#` character in the Antora docs, not as an admonition.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154431664
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,406 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData V4 services using standard OData CRUD operations.
+
+The component provides OData-specific request and response handling while using Camel HTTP as the underlying HTTP transport.
+
+The OData component is the successor to the deprecated `camel-olingo4` component.
+
+The component supports OData V4 services using JSON representations.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+[source,xml]
+
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-odata</artifactId>
+ <version>x.x.x</version>
+ <!-- use the same version as your Camel core version -->
+</dependency>
+----
+
+== URI Format
+
+=== odata:httpUri
+
+Where `httpUri` is the base URL of the OData entity set or resource.
+
+For example:
+
+[source,text]
+
+odata:http://localhost:8080/odata/Products
+odata:https://services.odata.org/V4/Northwind/Northwind.svc/Categories
+----------------------------------------------------------------------
+
+The URI can also contain endpoint options as query parameters.
+
+The OData query options can alternatively be configured on the endpoint or supplied dynamically using message headers.
+
+The component uses Camel HTTP for the underlying HTTP transport.
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+# [NOTE]
+
+# This is a producer-only component. Per-message options can be supplied through `CamelOData` headers in addition to endpoint options.
+
+# [NOTE]
+
+# If the route receives messages from an untrusted producer, take care when forwarding internal Camel headers to this component. In particular, an inbound `Authorization` header can be used for the OData request and takes precedence over endpoint authentication.
+
+== Operations
+
+The operation is selected using the `CamelODataOperation` message header or the `operation` endpoint option.
+
+The following operations are supported:
+
+[cols="20%,15%,65%",options="header"]
+|===
+| Operation | HTTP Method | Description
+
+| `READ_SET`
+| `GET`
+| Reads a collection of entities. This is the default operation.
+
+| `READ_ENTRY`
+| `GET`
+| Reads a single entity. Use `CamelODataKey` to specify the entity key.
+
+| `CREATE`
+| `POST`
+| Creates a new entity using the message body as the request payload.
+
+| `UPDATE`
+| `PATCH`
+| Updates an existing entity using `CamelODataKey` to identify the entity.
+
+| `DELETE`
+| `DELETE`
+| Deletes an existing entity using `CamelODataKey` to identify the entity.
+|===
+
+For `READ_ENTRY`, `UPDATE`, and `DELETE`, the key is appended to the configured resource URI.
+
+For example:
+
+[source,text]
+
+CamelODataKey = 101
+odata:http://localhost:8080/odata/Products
+------------------------------------------
+
+results in:
+
+[source,text]
+
+http://localhost:8080/odata/Products(101)
+
+The key is treated as an OData key expression. The component does not automatically add quotes or otherwise change the supplied key value.
+
+For example, a string key can be supplied as `'ABC-123'`, while a composite key can be supplied as `ProductID=101,CategoryID=5`.
+
+The key value should be a valid and trusted OData key expression.
+
+== Message Headers
+
+The following message headers can be used to control OData processing dynamically.
+
+[cols="25%,20%,55%",options="header"]
+|===
+| Header | Type | Description
+
+| `CamelODataOperation`
+| `ODataOperation` / `String`
+| Specifies the operation to execute.
+
+| `CamelODataKey`
+| `String`
+| Specifies the key for `READ_ENTRY`, `UPDATE`, and `DELETE` operations.
+
+| `CamelODataFilter`
+| `String`
+| Overrides the `$filter` query option.
+
+| `CamelODataSelect`
+| `String`
+| Overrides the `$select` query option.
+
+| `CamelODataExpand`
+| `String`
+| Overrides the `$expand` query option.
+
+| `CamelODataOrderBy`
+| `String`
+| Overrides the `$orderby` query option.
+
+| `CamelODataTop`
+| `Integer`
+| Overrides the `$top` query option.
+
+| `CamelODataSkip`
+| `Integer`
+| Overrides the `$skip` query option.
+
+| `CamelODataIncludeCount`
+| `Boolean`
+| Controls whether the `$count` query option is included in the request.
+
+| `CamelODataETag`
+| `String`
+| Supplies an ETag for the `If-Match` HTTP request header. The component also populates this header from an ETag returned by the OData service.
+
+| `CamelODataCount`
+| `Long`
+| Contains the count returned by the OData service when an `@odata.count` value is present.
+
+| `CamelODataNextLink`
+| `String`
+| Contains the `@odata.nextLink` value returned by the OData service when present.
+
+| `CamelHttpResponseCode`
+| `Integer`
+| Contains the HTTP response status code returned by the OData service.
+|===
+
+A message-level `Authorization` header can also be supplied.
+
+When an `Authorization` header is present, it takes precedence over authentication configured on the OData endpoint.
+
+If the OData route is exposed to an upstream HTTP consumer, such as `platform-http`, take care that an inbound `Authorization` header may be forwarded to the OData service.
+
+If the upstream authorization credentials must not be forwarded, remove the header before sending the exchange to the OData component.
+
+For example:
+
+[source,java]
+
+from("platform-http:/products")
+.removeHeader("Authorization")
+.to("odata:http://my-odata-service/odata/Products");
+----------------------------------------------------
+
+== Query Options
+
+The component supports the following OData query options:
+
+[cols="25%,20%,55%",options="header"]
+|===
+| Option | Type | Description
+
+| `filter`
+| `String`
+| OData `$filter` expression.
+
+| `select`
+| `String`
+| OData `$select` expression.
+
+| `expand`
+| `String`
+| OData `$expand` expression.
+
+| `orderBy`
+| `String`
+| OData `$orderby` expression.
+
+| `top`
+| `Integer`
+| OData `$top` value.
+
+| `skip`
+| `Integer`
+| OData `$skip` value.
+
+| `count`
+| `Boolean`
+| Controls the OData `$count` query option.
+|===
+
+For example:
+
+[source,text]
+
+odata:http://localhost:8080/odata/Products?filter=Name%20eq%20%27Laptop%27&select=Name,Price&top=10
+
+Query options configured through message headers take precedence over the corresponding endpoint configuration.
+
+== Authentication
+
+The component supports Basic authentication and Bearer token authentication through endpoint options.
+
+For example, Basic authentication can be configured using:
+
+[source,text]
+
+odata:http://localhost:8080/odata/Products?authMethod=Basic&authUsername=user&authPassword=password
+
+Bearer token authentication can be configured using the `authBearerToken` option.
+
+Authentication can also be supplied dynamically using the message-level `Authorization` header:
+
+[source,java]
+
+from("direct:getProducts")
+.setHeader("Authorization", constant("Bearer my-token"))
+.to("odata:http://my-odata-service/odata/Products");
+----------------------------------------------------
+
+When an `Authorization` header is present on the message, it takes precedence over endpoint authentication.
+
+Take care when forwarding exchanges from an upstream HTTP consumer because the incoming `Authorization` header may be forwarded to the OData service.
+
+== ETags
+
+The component supports optimistic concurrency using OData/HTTP ETags.
+
+An ETag supplied in `CamelODataETag` is sent to the service using the HTTP `If-Match` header.
+
+For example:
+
+[source,java]
+
+from("direct:updateProduct")
+.setHeader("CamelODataOperation", constant("UPDATE"))
+.setHeader("CamelODataKey", constant("101"))
+.setHeader("CamelODataETag", constant("W/"etag-value""))
Review Comment:
Addressed in the latest commit `ee3cc3b4f7a4`.
Both ETag examples now use the properly escaped Java string:
`.setHeader("CamelODataETag", constant("W/\"etag-value\""))`
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4161584246
##########
components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java:
##########
@@ -0,0 +1,1143 @@
+/*
+ * 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.odata;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.http.base.HttpOperationFailedException;
+import org.apache.camel.support.jsse.KeyStoreParameters;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.support.jsse.TrustManagersParameters;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.containing;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.matching;
+import static com.github.tomakehurst.wiremock.client.WireMock.patch;
+import static com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ODataProducerTest extends CamelTestSupport {
+
+ @RegisterExtension
+ static WireMockExtension wireMock = WireMockExtension.newInstance()
+ .options(wireMockConfig()
+ .dynamicPort()
+ .dynamicHttpsPort()
+ .keystorePath(Objects.requireNonNull(ODataProducerTest.class.getResource("/localhost.p12")).getFile())
+ .keystoreType("PKCS12")
+ .keystorePassword("changeit")
+ .keyManagerPassword("changeit"))
+ .build();
+
+ private ODataEndpoint endpoint;
+ private Producer producer;
+
+ @BeforeEach
+ public void setUpProducer() throws Exception {
+ String uri = "odata:http://localhost:" + wireMock.getPort() + "/odata/Products";
+ endpoint = context.getEndpoint(uri, ODataEndpoint.class);
+ producer = endpoint.createProducer();
+ producer.start();
+ }
+
+ @AfterEach
+ public void tearDownProducer() throws Exception {
+ if (producer != null) {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessGetCollection() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[{\"ID\":101,\"Name\":\"Laptop\"},{\"ID\":102,\"Name\":\"Phone\"}]}")));
+
+ Exchange exchange = endpoint.createExchange();
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertTrue(body.containsKey("value"));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products")));
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithKeyHeaderDynamicUri() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":101,\"Name\":\"Laptop\"}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "101");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertEquals("Laptop", body.get("Name"));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithKeyInUri() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":101,\"Name\":\"Laptop\"}")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertEquals("Laptop", body.get("Name"));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessQueryOptionsHeaders() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*Laptop.*"))
+ .withQueryParam("$select", matching(".*ID.*Name.*"))
+ .withQueryParam("$expand", equalTo("Category"))
+ .withQueryParam("$orderby", matching(".*Name.*asc.*"))
+ .withQueryParam("$top", equalTo("5"))
+ .withQueryParam("$skip", equalTo("10"))
+ .withQueryParam("$count", equalTo("true"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"@odata.count\":1,\"value\":[{\"ID\":101,\"Name\":\"Laptop\"}]}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.FILTER, "Name eq 'Laptop'");
+ exchange.getMessage().setHeader(ODataConstants.SELECT, "ID,Name");
+ exchange.getMessage().setHeader(ODataConstants.EXPAND, "Category");
+ exchange.getMessage().setHeader(ODataConstants.ORDER_BY, "Name asc");
+ exchange.getMessage().setHeader(ODataConstants.TOP, 5);
+ exchange.getMessage().setHeader(ODataConstants.SKIP, 10);
+ exchange.getMessage().setHeader(ODataConstants.INCLUDE_COUNT, true);
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals(1L, exchange.getMessage().getHeader(ODataConstants.COUNT));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*Laptop.*"))
+ .withQueryParam("$select", matching(".*ID.*Name.*"))
+ .withQueryParam("$expand", equalTo("Category"))
+ .withQueryParam("$orderby", matching(".*Name.*asc.*"))
+ .withQueryParam("$top", equalTo("5"))
+ .withQueryParam("$skip", equalTo("10"))
+ .withQueryParam("$count", equalTo("true")));
+ }
+
+ @Test
+ void testProcessCreateEntity() throws Exception {
+ wireMock.stubFor(post(urlPathEqualTo("/odata/Products"))
+ .withRequestBody(equalToJson("{\"Name\":\"Tablet\",\"Price\":299.99}"))
+ .willReturn(aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":103,\"Name\":\"Tablet\",\"Price\":299.99}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.CREATE);
+
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("Name", "Tablet");
+ payload.put("Price", 299.99);
+ exchange.getMessage().setBody(payload);
+
+ producer.process(exchange);
+
+ assertEquals(201, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertEquals(new BigDecimal(103), body.get("ID"));
+ }
+
+ @Test
+ void testProcessIgnoresInboundHttpRoutingHeaders() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange = endpoint.createExchange();
+
+ // Simulate HTTP/REST headers inherited from an upstream consumer.
+ exchange.getMessage().setHeader(Exchange.HTTP_URI, "http://malicious.example/other");
+ exchange.getMessage().setHeader(Exchange.HTTP_PATH, "/wrong/path");
+ exchange.getMessage().setHeader(Exchange.HTTP_QUERY, "unexpected=true");
+ exchange.getMessage().setHeader(Exchange.HTTP_RAW_QUERY, "rawUnexpected=true");
+ exchange.getMessage().setHeader("CamelRestHttpUri", "http://malicious.example/rest");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products")));
+ }
+
+ @Test
+ void testProcessUpdateEntity() throws Exception {
+ ODataEndpoint keyEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer keyProducer = keyEndpoint.createProducer();
+ keyProducer.start();
+
+ try {
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withRequestBody(equalToJson("{\"Name\":\"Updated Laptop\"}"))
+ .willReturn(aResponse().withStatus(204)));
+
+ Exchange exchange = keyEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.UPDATE);
+
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("Name", "Updated Laptop");
+ exchange.getMessage().setBody(payload);
+
+ keyProducer.process(exchange);
+
+ assertEquals(204, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ } finally {
+ keyProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessDeleteEntity() throws Exception {
+ ODataEndpoint keyEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer keyProducer = keyEndpoint.createProducer();
+ keyProducer.start();
+
+ try {
+ wireMock.stubFor(delete(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse().withStatus(204)));
+
+ Exchange exchange = keyEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.DELETE);
+
+ keyProducer.process(exchange);
+
+ assertEquals(204, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNull(exchange.getMessage().getBody());
+ } finally {
+ keyProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithETagHeader() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("\"etag-v1\""))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":101,\"Name\":\"Laptop\",\"@odata.etag\":\"etag-v2\"}")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.ETAG, "\"etag-v1\"");
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("etag-v2", exchange.getMessage().getHeader(ODataConstants.ETAG));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithHttpETag() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(102)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(102)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withHeader("ETag", "W/\"etag-v3\"")
+ .withBody("{\"ID\":102,\"Name\":\"Monitor\"}")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("W/\"etag-v3--gzip\"", exchange.getMessage().getHeader(ODataConstants.ETAG));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessUpdateEntityWithETag() throws Exception {
+ ODataEndpoint updateEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer updateProducer = updateEndpoint.createProducer();
+ updateProducer.start();
+
+ try {
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"etag-v3\""))
+ .withHeader("Content-Type", containing("application/json"))
+ .withRequestBody(equalToJson("{\"Name\":\"Updated Laptop\"}"))
+ .willReturn(aResponse()
+ .withStatus(204)
+ .withHeader("ETag", "W/\"etag-v4\"")));
+
+ Exchange exchange = updateEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.UPDATE);
+ exchange.getMessage().setHeader(ODataConstants.ETAG, "W/\"etag-v3\"");
+ exchange.getMessage().setBody("{\"Name\":\"Updated Laptop\"}");
+
+ updateProducer.process(exchange);
+
+ assertEquals(204, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("W/\"etag-v4\"", exchange.getMessage().getHeader(ODataConstants.ETAG));
+
+ wireMock.verify(patchRequestedFor(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"etag-v3\"")));
+ } finally {
+ updateProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessUpdateEntityWithETagPreconditionFailed() throws Exception {
+ ODataEndpoint updateEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer updateProducer = updateEndpoint.createProducer();
+ updateProducer.start();
+
+ try {
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"stale-etag\""))
+ .willReturn(aResponse()
+ .withStatus(412)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "error": {
+ "code": "412",
+ "message": "The ETag does not match."
+ }
+ }
+ """)));
+
+ Exchange exchange = updateEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.UPDATE);
+ exchange.getMessage().setHeader(ODataConstants.ETAG, "W/\"stale-etag\"");
+ exchange.getMessage().setBody("{\"Name\":\"Updated Laptop\"}");
+
+ HttpOperationFailedException exception = assertThrows(
+ HttpOperationFailedException.class,
+ () -> updateProducer.process(exchange));
+
+ assertEquals(412, exception.getStatusCode());
+ wireMock.verify(patchRequestedFor(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"stale-etag\"")));
+ } finally {
+ updateProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadSetWithEncodedFilter() throws Exception {
+ ODataEndpoint readEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer readProducer = readEndpoint.createProducer();
+ readProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo("Name eq 'Laptop Pro'"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "value": [
+ {"ID": 101, "Name": "Laptop Pro"}
+ ]
+ }
+ """)));
+
+ Exchange exchange = readEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_SET);
+ exchange.getMessage().setHeader(ODataConstants.FILTER, "Name eq 'Laptop Pro'");
+
+ readProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo("Name eq 'Laptop Pro'")));
+ } finally {
+ readProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadSetWithComplexEncodedFilter() throws Exception {
+ ODataEndpoint readEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer readProducer = readEndpoint.createProducer();
+ readProducer.start();
+
+ String filter = "Name eq 'O''Reilly' and Category eq 'Office/Supplies' "
+ + "and Price gt 10.5 and (Status eq 'Active' or Code eq 'A%20B') "
+ + "and startswith(Name,'Pro')";
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo(filter))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "value": [
+ {
+ "ID": 101,
+ "Name": "O'Reilly Pro",
+ "Category": "Office/Supplies"
+ }
+ ]
+ }
+ """)));
+
+ Exchange exchange = readEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_SET);
+ exchange.getMessage().setHeader(ODataConstants.FILTER, filter);
+
+ readProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo(filter)));
+ } finally {
+ readProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadEntryWithTrailingSlashAndKey() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products/",
+ ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "ID": 101,
+ "Name": "Laptop"
+ }
+ """)));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "101");
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessNextLinkHeader() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[],\"@odata.nextLink\":\"http://localhost/odata/Products?$skip=10\"}")));
+
+ Exchange exchange = endpoint.createExchange();
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("http://localhost/odata/Products?$skip=10", exchange.getMessage().getHeader(ODataConstants.NEXT_LINK));
+ }
+
+ @Test
+ void testProcessEndpointConfigurationDefaults() throws Exception {
+ ODataEndpoint configuredEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?top=5&skip=10&filter=Name eq 'Phone'",
+ ODataEndpoint.class);
+ Producer configuredProducer = configuredEndpoint.createProducer();
+ configuredProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$top", equalTo("5"))
+ .withQueryParam("$skip", equalTo("10"))
+ .withQueryParam("$filter", matching(".*Name.*Phone.*"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[{\"ID\":102,\"Name\":\"Phone\"}]}")));
+
+ Exchange exchange = configuredEndpoint.createExchange();
+ configuredProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ } finally {
+ configuredProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessHeaderOverridesEndpointConfiguration() throws Exception {
+ ODataEndpoint configuredEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?top=5",
+ ODataEndpoint.class);
+ Producer configuredProducer = configuredEndpoint.createProducer();
+ configuredProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$top", equalTo("20"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange = configuredEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.TOP, 20);
+
+ configuredProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ } finally {
+ configuredProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessHttpErrorPropagatesException() {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse().withStatus(500)));
+
+ Exchange exchange = endpoint.createExchange();
+
+ assertThrows(HttpOperationFailedException.class, () -> producer.process(exchange));
+ }
+
+ @Test
+ void testProcessEmptyResponseBodyReturnsNullBody() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNull(exchange.getMessage().getBody());
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProducerSingletonAndLifecycle() {
+ assertTrue(producer.isSingleton());
+ }
+
+ @Test
+ void testBasicAuthentication() throws Exception {
+ String username = "adminUser";
+ String password = "adminPassword123";
+ String expectedHeader
+ = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
+
+ ODataEndpoint authEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authMethod=Basic&authUsername=" + username
+ + "&authPassword=" + password,
+ ODataEndpoint.class);
+ Producer authProducer = authEndpoint.createProducer();
+ authProducer.start();
+
+ try {
+ // Priority 1: Match and return 200 OK when correct credentials are present
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(1)
+ .withHeader("Authorization", equalTo(expectedHeader))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ // Priority 10: Fallback HTTP Basic 401 challenge with WWW-Authenticate header
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(10)
+ .willReturn(aResponse()
+ .withStatus(401)
+ .withHeader("WWW-Authenticate", "Basic realm=\"OData Test Realm\"")));
+
+ Exchange exchange = authEndpoint.createExchange();
+ authProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(expectedHeader)));
+ } finally {
+ authProducer.stop();
+ }
+ }
+
+ @Test
+ void testBearerAuthenticationAndExchangeHeaderPrecedence() throws Exception {
+ String configuredToken = "configuredBearerToken123";
+ String explicitHeaderToken = "Bearer explicitHeaderToken456";
+
+ ODataEndpoint authEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authBearerToken=" + configuredToken,
+ ODataEndpoint.class);
+ Producer authProducer = authEndpoint.createProducer();
+ authProducer.start();
+
+ try {
+ // Case 1: Configured bearer token attached automatically
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + configuredToken))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange1 = authEndpoint.createExchange();
+ authProducer.process(exchange1);
+ assertEquals(200, exchange1.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + configuredToken)));
+
+ // Case 2: Explicit Exchange Authorization header takes precedence over configured token
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(explicitHeaderToken))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange2 = authEndpoint.createExchange();
+ exchange2.getMessage().setHeader("Authorization", explicitHeaderToken);
+ authProducer.process(exchange2);
+ assertEquals(200, exchange2.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(explicitHeaderToken)));
+ } finally {
+ authProducer.stop();
+ }
+ }
+
+ @Test
+ void testBearerEndpointConcurrentIsolation() throws Exception {
+ String token1 = "tokenUnit1";
+ String token2 = "tokenUnit2";
+
+ ODataEndpoint endpoint1 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?authBearerToken=" + token1,
+ ODataEndpoint.class);
+ ODataEndpoint endpoint2 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?authBearerToken=" + token2,
+ ODataEndpoint.class);
+
+ Producer producer1 = endpoint1.createProducer();
+ Producer producer2 = endpoint2.createProducer();
+
+ producer1.start();
+ producer2.start();
+
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ int threadCount = 10;
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ ConcurrentLinkedQueue<Throwable> failures = new ConcurrentLinkedQueue<>();
+
+ try {
+ for (int i = 0; i < threadCount; i++) {
+ final int index = i;
+ executor.submit(() -> {
+ Exchange exchange = null;
+ try {
+ Producer targetProducer = (index % 2 == 0) ? producer1 : producer2;
+ ODataEndpoint targetEndpoint = (index % 2 == 0) ? endpoint1 : endpoint2;
+ exchange = targetEndpoint.createExchange();
+ targetProducer.process(exchange);
+ if (exchange.getException() != null) {
+ failures.add(exchange.getException());
+ }
+ } catch (Throwable t) {
+ failures.add(t);
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ assertTrue(failures.isEmpty(), "Worker threads encountered exceptions: " + failures);
+
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + token1)));
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + token2)));
+ } finally {
+ executor.shutdownNow();
+ producer1.stop();
+ producer2.stop();
+ }
+ }
+
+ @Test
+ void testBasicAuthConcurrentIsolation() throws Exception {
+ String user1 = "user1";
+ String pass1 = "pass1";
+ String expectedHeader1
+ = "Basic " + Base64.getEncoder().encodeToString((user1 + ":" + pass1).getBytes(StandardCharsets.UTF_8));
+
+ String user2 = "user2";
+ String pass2 = "pass2";
+ String expectedHeader2
+ = "Basic " + Base64.getEncoder().encodeToString((user2 + ":" + pass2).getBytes(StandardCharsets.UTF_8));
+
+ ODataEndpoint endpoint1 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authMethod=Basic&authUsername=" + user1
+ + "&authPassword=" + pass1,
+ ODataEndpoint.class);
+ ODataEndpoint endpoint2 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authMethod=Basic&authUsername=" + user2
+ + "&authPassword=" + pass2,
+ ODataEndpoint.class);
+
+ Producer producer1 = endpoint1.createProducer();
+ Producer producer2 = endpoint2.createProducer();
+
+ producer1.start();
+ producer2.start();
+
+ int threadCount = 10;
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+
+ try {
+ // Priority 1: Match and return 200 OK when user1 credentials are present
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(1)
+ .withHeader("Authorization", equalTo(expectedHeader1))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ // Priority 1: Match and return 200 OK when user2 credentials are present
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(1)
+ .withHeader("Authorization", equalTo(expectedHeader2))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ // Priority 10: Fallback HTTP Basic 401 challenge with WWW-Authenticate header
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(10)
+ .willReturn(aResponse()
+ .withStatus(401)
+ .withHeader("WWW-Authenticate", "Basic realm=\"OData Test Realm\"")));
+
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ ConcurrentLinkedQueue<Throwable> failures = new ConcurrentLinkedQueue<>();
+
+ for (int i = 0; i < threadCount; i++) {
+ final int index = i;
+ executor.submit(() -> {
+ Exchange exchange = null;
+ try {
+ Producer targetProducer = (index % 2 == 0) ? producer1 : producer2;
+ ODataEndpoint targetEndpoint = (index % 2 == 0) ? endpoint1 : endpoint2;
+ exchange = targetEndpoint.createExchange();
+ targetProducer.process(exchange);
+ if (exchange.getException() != null) {
+ failures.add(exchange.getException());
+ }
+ } catch (Throwable t) {
+ failures.add(t);
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ assertTrue(failures.isEmpty(), "Worker threads encountered exceptions: " + failures);
+
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(expectedHeader1)));
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(expectedHeader2)));
+ } finally {
+ executor.shutdownNow();
+ producer1.stop();
+ producer2.stop();
+ }
+ }
+
+ @Test
+ void testQueryEncodingWithAmpersand() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*John.*Sons.*"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.FILTER, "Name eq 'John & Sons'");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*John.*Sons.*")));
+ }
+
+ @Test
+ void testProcessReadEntryWithStringKey() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer producer = endpoint.createProducer();
+ producer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products('ABC-123')"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {"ID":"ABC-123","Name":"Laptop"}
+ """)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "'ABC-123'");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products('ABC-123')")));
+ } finally {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadEntryWithCompositeKey() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer producer = endpoint.createProducer();
+ producer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo(
+ "/odata/Products(ProductID=101,CategoryID=5)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {"ProductID":101,"CategoryID":5,"Name":"Laptop"}
+ """)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(
+ ODataConstants.KEY, "ProductID=101,CategoryID=5");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo(
+ "/odata/Products(ProductID=101,CategoryID=5)")));
+ } finally {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessWithConfiguredBasicAuthentication() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+
+ endpoint.getConfiguration().setAuthMethod("Basic");
+ endpoint.getConfiguration().setAuthUsername("odata-user");
+ endpoint.getConfiguration().setAuthPassword("odata-password");
+
+ Producer producer = endpoint.createProducer();
+ producer.start();
+
+ try {
+ String credentials = Base64.getEncoder()
+ .encodeToString("odata-user:odata-password".getBytes(StandardCharsets.UTF_8));
+
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Basic " + credentials))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "value": []
+ }
+ """)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_SET);
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Basic " + credentials)));
+ } finally {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessHttpsWithSslContextParameters() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ KeyStoreParameters trustStore = new KeyStoreParameters();
+ trustStore.setResource("localhost.p12");
+ trustStore.setPassword("changeit");
+ trustStore.setType("PKCS12");
+
+ TrustManagersParameters trustManagers = new TrustManagersParameters();
+ trustManagers.setKeyStore(trustStore);
+
+ SSLContextParameters sslContextParameters = new SSLContextParameters();
+ sslContextParameters.setTrustManagers(trustManagers);
+
+ String baseUri = "odata:https://localhost:" + wireMock.getHttpsPort() + "/odata/Products";
+
+ // Use different OData endpoint URIs while keeping the underlying HTTP URI identical.
+ ODataEndpoint httpsEndpoint = context.getEndpoint(
+ baseUri + "?useGlobalSslContextParameters=true", ODataEndpoint.class);
+ httpsEndpoint.setSslContextParameters(sslContextParameters);
+
+ ODataEndpoint endpointWithoutSsl = context.getEndpoint(
+ baseUri + "?useGlobalSslContextParameters=false", ODataEndpoint.class);
+
+ Producer httpsProducer = httpsEndpoint.createProducer();
+ Producer endpointWithoutSslProducer = endpointWithoutSsl.createProducer();
+
+ try {
+ httpsProducer.start();
+
+ Exchange exchange = httpsEndpoint.createExchange();
+ httpsProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNotNull(exchange.getMessage().getBody());
+
+ endpointWithoutSslProducer.start();
+
+ Exchange exchangeWithoutSsl = endpointWithoutSsl.createExchange();
+
+ Throwable thrown = assertThrows(Throwable.class, () -> endpointWithoutSslProducer.process(exchangeWithoutSsl));
+
+ // Check for SSLException / SSLHandshakeException in the cause chain
+ boolean hasSslCause = false;
+ Throwable current = thrown;
+ while (current != null) {
+ if (current instanceof javax.net.ssl.SSLException) { // Use jakarta.net.ssl.SSLException if on Jakarta/Camel 4+
+ hasSslCause = true;
Review Comment:
💡 **Nit (non-blocking):** The comment `// Use jakarta.net.ssl.SSLException if on Jakarta/Camel 4+` is misleading — `javax.net.ssl.SSLException` is a JDK class (in the `java.base` module), not part of Jakarta EE. It has never been and will never be renamed to `jakarta.net.ssl`. The actual code is correct; just the comment is wrong.
```suggestion
if (current instanceof javax.net.ssl.SSLException) {
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on PR #27168:
URL: https://github.com/apache/camel/pull/27168#issuecomment-5942800110
> yeah lets avoid using guava
addressed in commit 4ac88cda5073
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4156236395
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,408 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData V4 services using standard OData CRUD operations.
+
+The component provides OData-specific request and response handling while using Camel HTTP as the underlying HTTP transport.
+
+The OData component is the successor to the deprecated `camel-olingo4` component.
+
+The component supports OData V4 services using JSON representations.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+[source,xml]
+
+<dependency>
Review Comment:
Addressed in commit c1a11055ded4.
Added the required opening and closing ---- delimiters to all [source,*] listing blocks in the OData component documentation and verified the file with git diff --check.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154133985
##########
components/camel-odata/pom.xml:
##########
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>components</artifactId>
+ <version>4.23.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>camel-odata</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Camel :: OData</name>
+ <description>Camel OData Component</description>
+
+ <properties>
+ <firstVersion>4.23.0-SNAPSHOT</firstVersion>
Review Comment:
Addressed in the latest commit 4cb8cea36f16.
firstVersion is now set to 4.23.0 in the generated OData component metadata.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4155221662
##########
components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataEndpointTest.java:
##########
@@ -0,0 +1,95 @@
+/*
+ * 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.odata;
+
+import java.net.URI;
+
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ODataEndpointTest extends CamelTestSupport {
+
+ @Test
+ void testEndpointUriConfiguration() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:8080/odata/Products", ODataEndpoint.class);
+
+ assertNotNull(endpoint);
+ assertEquals(new URI("http://localhost:8080/odata/Products"), endpoint.getHttpUri());
+ assertNotNull(endpoint.createProducer());
+ }
+
+ @Test
+ void testEndpointWithQueryParameters() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:8080/odata/Products?top=10&filter=Name%20eq%20'Test'", ODataEndpoint.class);
+
+ assertNotNull(endpoint);
+ assertEquals(new URI("http://localhost:8080/odata/Products"), endpoint.getHttpUri());
+ assertEquals(10, endpoint.getConfiguration().getTop());
+ assertEquals("Name eq 'Test'", endpoint.getConfiguration().getFilter());
+ }
+
+ @Test
+ void testEndpointSchemeAndMetadata() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:8080/odata/Products", ODataEndpoint.class);
+
+ assertEquals("odata", endpoint.getEndpointKey().split(":")[0]);
+ assertEquals("odata://http://localhost:8080/odata/Products", endpoint.getEndpointUri());
+ }
+
+ @Test
+ void testSecretMaskingInUriAndToString() {
+ String rawUri
+ = "odata:http://localhost:8080/odata/Products?authUsername=user&authPassword=secretPassword&authBearerToken=myToken";
+ ODataEndpoint endpoint = context.getEndpoint(rawUri, ODataEndpoint.class);
+
+ assertNotNull(endpoint);
+
+ // toString() must mask secrets
+ String toString = endpoint.toString();
+ assertFalse(toString.contains("secretPassword"), "toString() should not reveal authPassword");
+ assertFalse(toString.contains("myToken"), "toString() should not reveal authBearerToken");
+ assertTrue(toString.contains("authPassword=xxxxxx") || toString.contains("authPassword=******"));
+
+ // Check URISupport sanitization directly if testing Camel's URI masking utility
+ String sanitizedUri = org.apache.camel.util.URISupport.sanitizeUri(endpoint.getEndpointUri());
Review Comment:
Small: please import `org.apache.camel.util.URISupport` instead of using the fully qualified name (CLAUDE.md, Import Style).
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java:
##########
@@ -0,0 +1,296 @@
+/*
+ * 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.odata;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.component.http.HttpConstants;
+import org.apache.camel.support.DefaultProducer;
+import org.apache.camel.support.MessageHelper;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.util.URISupport;
+
+public class ODataProducer extends DefaultProducer {
+
+ private final ODataEndpoint endpoint;
+ private Producer httpProducer;
+
+ public ODataProducer(ODataEndpoint endpoint) {
+ super(endpoint);
+ this.endpoint = endpoint;
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ super.doStart();
+
+ URI uri = endpoint.getHttpUri();
+ String httpUri = uri.toString();
+
+ if (httpUri.contains("?")) {
+ httpUri += "&throwExceptionOnFailure=true";
+ } else {
+ httpUri += "?throwExceptionOnFailure=true";
+ }
+
+ ODataConfiguration config = endpoint.getConfiguration();
+ Map<String, Object> httpParameters = new LinkedHashMap<>();
+
+ if (config != null) {
+ SSLContextParameters sslParams = endpoint.getSslContextParameters();
+ if (sslParams == null && endpoint.isUseGlobalSslContextParameters()) {
+ sslParams = endpoint.getCamelContext().getSSLContextParameters();
+ }
+ if (sslParams != null) {
+ httpParameters.put("sslContextParameters", sslParams);
+ }
+ }
+
+ Endpoint httpEndpoint = endpoint.getCamelContext()
Review Comment:
`CamelContext.getEndpoint(uri, parameters)` only uses `parameters` when the endpoint is created: the lookup is keyed by the normalized URI alone (`AbstractCamelContext.doGetEndpoint`). So two odata endpoints with the same base URL but different `sslContextParameters` (or one using the global SSL context and one not) share whichever HTTP endpoint was created first, and the second endpoint's TLS settings are silently ignored. A user's own `https://...?throwExceptionOnFailure=true` endpoint would collide the same way.
Creating a private HTTP endpoint avoids the cache, e.g.
```java
Component http = getEndpoint().getCamelContext().getComponent(uri.getScheme());
httpEndpoint = http.createEndpoint(httpUri, httpParameters);
ServiceHelper.startService(httpEndpoint);
```
and stop it in `doStop()`. A test with two endpoints on the same URL, one with a trust store and one without, would cover it.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "apupier (via GitHub)" <gi...@apache.org>.
apupier commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4164412035
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,414 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData V4 services using standard OData CRUD operations.
+
+The component provides OData-specific request and response handling while using Camel HTTP as the underlying HTTP transport.
+
+The OData component is the successor to the deprecated `camel-olingo4` component.
+
+The component supports OData V4 services using JSON representations.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-odata</artifactId>
+ <version>x.x.x</version>
+ <!-- use the same version as your Camel core version -->
+</dependency>
+----
+
+== URI Format
+
+=== odata:httpUri
+
+Where `httpUri` is the base URL of the OData entity set or resource.
+
+For example:
+
+[source,text]
+----
+odata:http://localhost:8080/odata/Products
+odata:https://services.odata.org/V4/Northwind/Northwind.svc/Categories
+----
+
+The URI can also contain endpoint options as query parameters.
+
+The OData query options can alternatively be configured on the endpoint or supplied dynamically using message headers.
+
+The component uses Camel HTTP for the underlying HTTP transport.
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+[NOTE]
+====
+This is a producer-only component. Per-message options can be supplied through `CamelOData` headers in addition to endpoint options.
+====
+
+[NOTE]
+====
+If the route receives messages from an untrusted producer, take care when forwarding internal Camel headers to this component. In particular, an inbound `Authorization` header can be used for the OData request and takes precedence over endpoint authentication.
+====
+
+== Operations
+
+The operation is selected using the `CamelODataOperation` message header or the `operation` endpoint option.
+
+The following operations are supported:
+
+[cols="20%,15%,65%",options="header"]
+|===
+| Operation | HTTP Method | Description
+
+| `READ_SET`
+| `GET`
+| Reads a collection of entities. This is the default operation.
+
+| `READ_ENTRY`
+| `GET`
+| Reads a single entity. Use `CamelODataKey` to specify the entity key.
+
+| `CREATE`
+| `POST`
+| Creates a new entity using the message body as the request payload.
+
+| `UPDATE`
+| `PATCH`
+| Updates an existing entity using `CamelODataKey` to identify the entity.
+
+| `DELETE`
+| `DELETE`
+| Deletes an existing entity using `CamelODataKey` to identify the entity.
+|===
+
+For `READ_ENTRY`, `UPDATE`, and `DELETE`, the key is appended to the configured resource URI.
+
+For example:
+
+[source,text]
+----
+CamelODataKey = 101
+odata:http://localhost:8080/odata/Products
+----
+
+results in:
+
+[source,text]
+----
+http://localhost:8080/odata/Products(101)
+----
+
+The key is treated as an OData key expression. The component does not automatically add quotes or otherwise change the supplied key value.
+
+For example, a string key can be supplied as `'ABC-123'`, while a composite key can be supplied as `ProductID=101,CategoryID=5`.
+
+The key value should be a valid and trusted OData key expression.
+
+== Message Headers
+
+The following message headers can be used to control OData processing dynamically.
+
+[cols="25%,20%,55%",options="header"]
+|===
+| Header | Type | Description
+
+| `CamelODataOperation`
+| `ODataOperation` / `String`
+| Specifies the operation to execute.
+
+| `CamelODataKey`
+| `String`
+| Specifies the key for `READ_ENTRY`, `UPDATE`, and `DELETE` operations.
+
+| `CamelODataFilter`
+| `String`
+| Overrides the `$filter` query option.
+
+| `CamelODataSelect`
+| `String`
+| Overrides the `$select` query option.
+
+| `CamelODataExpand`
+| `String`
+| Overrides the `$expand` query option.
+
+| `CamelODataOrderBy`
+| `String`
+| Overrides the `$orderby` query option.
+
+| `CamelODataTop`
+| `Integer`
+| Overrides the `$top` query option.
+
+| `CamelODataSkip`
+| `Integer`
+| Overrides the `$skip` query option.
+
+| `CamelODataIncludeCount`
+| `Boolean`
+| Controls whether the `$count` query option is included in the request.
+
+| `CamelODataETag`
+| `String`
+| Supplies an ETag for the `If-Match` HTTP request header. The component also populates this header from an ETag returned by the OData service.
+
+| `CamelODataCount`
+| `Long`
+| Contains the count returned by the OData service when an `@odata.count` value is present.
+
+| `CamelODataNextLink`
+| `String`
+| Contains the `@odata.nextLink` value returned by the OData service when present.
+
+| `CamelHttpResponseCode`
+| `Integer`
+| Contains the HTTP response status code returned by the OData service.
+|===
+
+A message-level `Authorization` header can also be supplied.
+
+When an `Authorization` header is present, it takes precedence over authentication configured on the OData endpoint.
+
+If the OData route is exposed to an upstream HTTP consumer, such as `platform-http`, take care that an inbound `Authorization` header may be forwarded to the OData service.
+
+If the upstream authorization credentials must not be forwarded, remove the header before sending the exchange to the OData component.
+
+For example:
+
+[source,java]
+----
+from("platform-http:/products")
+.removeHeader("Authorization")
+.to("odata:http://my-odata-service/odata/Products");
+----
+
+== Query Options
+
+The component supports the following OData query options:
+
+[cols="25%,20%,55%",options="header"]
+|===
+| Option | Type | Description
+
+| `filter`
+| `String`
+| OData `$filter` expression.
+
+| `select`
+| `String`
+| OData `$select` expression.
+
+| `expand`
+| `String`
+| OData `$expand` expression.
+
+| `orderBy`
+| `String`
+| OData `$orderby` expression.
+
+| `top`
+| `Integer`
+| OData `$top` value.
+
+| `skip`
+| `Integer`
+| OData `$skip` value.
+
+| `count`
+| `Boolean`
+| Controls the OData `$count` query option.
+|===
+
+Additional OData query options can be supplied as endpoint URI parameters. Because the component uses lenient endpoint properties, options that are not defined as explicit component options are passed through as OData query parameters. Ensure that endpoint option names are spelled correctly and that any additional options are valid OData query options.
+
+For example:
+
+[source,text]
+----
+odata:http://localhost:8080/odata/Products?filter=Name%20eq%20%27Laptop%27&select=Name,Price&top=10
+----
+
+Query options configured through message headers take precedence over the corresponding endpoint configuration.
+
+== Authentication
+
+The component supports Basic authentication and Bearer token authentication through endpoint options.
+
+For example, Basic authentication can be configured using:
+
+[source,text]
+----
+odata:http://localhost:8080/odata/Products?authMethod=Basic&authUsername=user&authPassword=password
+----
+
+Bearer token authentication can be configured using the `authBearerToken` option.
+
+Authentication can also be supplied dynamically using the message-level `Authorization` header:
+
+[source,java]
+----
+from("direct:getProducts")
+.setHeader("Authorization", constant("Bearer my-token"))
+.to("odata:http://my-odata-service/odata/Products");
+----
Review Comment:
optional: it is better when we add the 3 dsl languages examples. This can be done in another iteration.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4156209888
##########
components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataEndpointTest.java:
##########
@@ -0,0 +1,95 @@
+/*
+ * 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.odata;
+
+import java.net.URI;
+
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ODataEndpointTest extends CamelTestSupport {
+
+ @Test
+ void testEndpointUriConfiguration() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:8080/odata/Products", ODataEndpoint.class);
+
+ assertNotNull(endpoint);
+ assertEquals(new URI("http://localhost:8080/odata/Products"), endpoint.getHttpUri());
+ assertNotNull(endpoint.createProducer());
+ }
+
+ @Test
+ void testEndpointWithQueryParameters() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:8080/odata/Products?top=10&filter=Name%20eq%20'Test'", ODataEndpoint.class);
+
+ assertNotNull(endpoint);
+ assertEquals(new URI("http://localhost:8080/odata/Products"), endpoint.getHttpUri());
+ assertEquals(10, endpoint.getConfiguration().getTop());
+ assertEquals("Name eq 'Test'", endpoint.getConfiguration().getFilter());
+ }
+
+ @Test
+ void testEndpointSchemeAndMetadata() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:8080/odata/Products", ODataEndpoint.class);
+
+ assertEquals("odata", endpoint.getEndpointKey().split(":")[0]);
+ assertEquals("odata://http://localhost:8080/odata/Products", endpoint.getEndpointUri());
+ }
+
+ @Test
+ void testSecretMaskingInUriAndToString() {
+ String rawUri
+ = "odata:http://localhost:8080/odata/Products?authUsername=user&authPassword=secretPassword&authBearerToken=myToken";
+ ODataEndpoint endpoint = context.getEndpoint(rawUri, ODataEndpoint.class);
+
+ assertNotNull(endpoint);
+
+ // toString() must mask secrets
+ String toString = endpoint.toString();
+ assertFalse(toString.contains("secretPassword"), "toString() should not reveal authPassword");
+ assertFalse(toString.contains("myToken"), "toString() should not reveal authBearerToken");
+ assertTrue(toString.contains("authPassword=xxxxxx") || toString.contains("authPassword=******"));
+
+ // Check URISupport sanitization directly if testing Camel's URI masking utility
+ String sanitizedUri = org.apache.camel.util.URISupport.sanitizeUri(endpoint.getEndpointUri());
Review Comment:
Addressed in commit `c1a11055ded4`.
Imported `org.apache.camel.util.URISupport` and replaced the fully qualified reference with `URISupport.sanitizeUri(...)`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #27168:
URL: https://github.com/apache/camel/pull/27168#issuecomment-5925308535
<!-- regen-patch -->
### :warning: Generated files are out of date
The [CI build](https://github.com/apache/camel/actions/runs/36818860934) regenerated files that are not committed on this PR.
If *Allow edits from maintainers* is enabled, a maintainer bot will automatically
push a "Regen" commit to this branch shortly.
Otherwise, you can apply the patch manually — the missing changes are available
as the `regen-patch` artifact of that run:
```bash
gh pr checkout 27168 --repo apache/camel
gh run download 36818860934 --repo apache/camel -n regen-patch
git apply --index regen.patch && rm regen.patch pr-number
git commit -m "Regen" && git push
```
Alternatively, run `./etc/scripts/regen.sh` locally and commit the resulting changes.
If the patch does not apply cleanly, update the branch from the base branch first —
the patch was generated against the merge with the base branch.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154154242
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,359 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData (Open Data Protocol) services using standard OData CRUD operations.
+
+The component delegates HTTP transport to Camel HTTP while providing OData-specific operation, key, query option, authentication, and response handling.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+## [source,xml]
Review Comment:
Addressed in the latest commit `4cb8cea36f16`.
* Corrected all code listing blocks from `## [source,...]` to valid `[source,...]` AsciiDoc syntax with proper delimiters.
* Fixed the unescaped quotes in both ETag Java examples.
* Ran `git diff --check` and the OData component tests successfully.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on PR #27168:
URL: https://github.com/apache/camel/pull/27168#issuecomment-5947401276
@davsclaus @gnodet Is there anything else pending that needs to be addressed before this PR can be merged?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154435970
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,408 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData V4 services using standard OData CRUD operations.
+
+The component provides OData-specific request and response handling while using Camel HTTP as the underlying HTTP transport.
+
+The OData component is the successor to the deprecated `camel-olingo4` component.
+
+The component supports OData V4 services using JSON representations.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+[source,xml]
+
+<dependency>
Review Comment:
🔴 **AsciiDoc listing blocks need opening `----` delimiters.** Every `[source,*]` block in this file is missing its opening `----`. The AsciiDoc spec requires both an opening and closing `----` around the content when using listing block syntax.
For example, this block (and all 16 others like it) should be:
```suggestion
[source,xml]
----
<dependency>
```
The current pattern — `[source,xml]` followed by a blank line and then content — causes the Antora renderer to produce the `unterminated listing block` warning that the docs CI job already flagged (see @apupier's comment).
The same fix is needed for all 17 `[source,*]` blocks in the file: lines 25, 43, 106, 114, 195, 241, 253, 261, 280, 300, 316, 336, 348, 358, 364, 376, 392. Five of those blocks (114, 241, 253, 300, 358) are also missing a closing `----`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154423987
##########
catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/odata.json:
##########
@@ -0,0 +1,62 @@
+{
+ "component": {
+ "kind": "component",
+ "name": "odata",
+ "title": "OData",
+ "description": "Camel OData Component",
+ "deprecated": false,
+ "firstVersion": "4.23.0",
Review Comment:
Addressed in the latest commit.
* Regenerated component metadata now reflects `Category.HTTP` only and marks `authPassword` and `authBearerToken` as secrets.
* Removed the stale catalog `others/odata.json`.
* Confirmed there is no `odata` entry remaining in the catalog `others.properties`.
* The generated component registration and catalog metadata are now aligned with the OData component.
The changes were rebased onto the latest `Regen` commit and pushed to the PR branch.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4163319349
##########
components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java:
##########
@@ -0,0 +1,1143 @@
+/*
+ * 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.odata;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.http.base.HttpOperationFailedException;
+import org.apache.camel.support.jsse.KeyStoreParameters;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.support.jsse.TrustManagersParameters;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.containing;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.matching;
+import static com.github.tomakehurst.wiremock.client.WireMock.patch;
+import static com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ODataProducerTest extends CamelTestSupport {
+
+ @RegisterExtension
+ static WireMockExtension wireMock = WireMockExtension.newInstance()
+ .options(wireMockConfig()
+ .dynamicPort()
+ .dynamicHttpsPort()
+ .keystorePath(Objects.requireNonNull(ODataProducerTest.class.getResource("/localhost.p12")).getFile())
Review Comment:
**Nit (optional, non-blocking):** `URL.getFile()` keeps URL encoding (e.g. `%20`), so WireMock can't open the keystore when the repo is checked out in a path containing spaces. Going through the URI avoids that, e.g. `Paths.get(url.toURI()).toString()`. Since `toURI()` throws a checked exception, that would need a small static helper, so it's also fine to keep `getFile()` and accept the edge case.
##########
components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java:
##########
@@ -0,0 +1,1143 @@
+/*
+ * 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.odata;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.http.base.HttpOperationFailedException;
+import org.apache.camel.support.jsse.KeyStoreParameters;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.support.jsse.TrustManagersParameters;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.containing;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.matching;
+import static com.github.tomakehurst.wiremock.client.WireMock.patch;
+import static com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ODataProducerTest extends CamelTestSupport {
+
+ @RegisterExtension
+ static WireMockExtension wireMock = WireMockExtension.newInstance()
+ .options(wireMockConfig()
+ .dynamicPort()
+ .dynamicHttpsPort()
+ .keystorePath(Objects.requireNonNull(ODataProducerTest.class.getResource("/localhost.p12")).getFile())
+ .keystoreType("PKCS12")
+ .keystorePassword("changeit")
+ .keyManagerPassword("changeit"))
+ .build();
+
+ private ODataEndpoint endpoint;
+ private Producer producer;
+
+ @BeforeEach
+ public void setUpProducer() throws Exception {
+ String uri = "odata:http://localhost:" + wireMock.getPort() + "/odata/Products";
+ endpoint = context.getEndpoint(uri, ODataEndpoint.class);
+ producer = endpoint.createProducer();
+ producer.start();
+ }
+
+ @AfterEach
+ public void tearDownProducer() throws Exception {
+ if (producer != null) {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessGetCollection() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[{\"ID\":101,\"Name\":\"Laptop\"},{\"ID\":102,\"Name\":\"Phone\"}]}")));
+
+ Exchange exchange = endpoint.createExchange();
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertTrue(body.containsKey("value"));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products")));
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithKeyHeaderDynamicUri() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":101,\"Name\":\"Laptop\"}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "101");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertEquals("Laptop", body.get("Name"));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithKeyInUri() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":101,\"Name\":\"Laptop\"}")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertEquals("Laptop", body.get("Name"));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessQueryOptionsHeaders() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*Laptop.*"))
+ .withQueryParam("$select", matching(".*ID.*Name.*"))
+ .withQueryParam("$expand", equalTo("Category"))
+ .withQueryParam("$orderby", matching(".*Name.*asc.*"))
+ .withQueryParam("$top", equalTo("5"))
+ .withQueryParam("$skip", equalTo("10"))
+ .withQueryParam("$count", equalTo("true"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"@odata.count\":1,\"value\":[{\"ID\":101,\"Name\":\"Laptop\"}]}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.FILTER, "Name eq 'Laptop'");
+ exchange.getMessage().setHeader(ODataConstants.SELECT, "ID,Name");
+ exchange.getMessage().setHeader(ODataConstants.EXPAND, "Category");
+ exchange.getMessage().setHeader(ODataConstants.ORDER_BY, "Name asc");
+ exchange.getMessage().setHeader(ODataConstants.TOP, 5);
+ exchange.getMessage().setHeader(ODataConstants.SKIP, 10);
+ exchange.getMessage().setHeader(ODataConstants.INCLUDE_COUNT, true);
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals(1L, exchange.getMessage().getHeader(ODataConstants.COUNT));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*Laptop.*"))
+ .withQueryParam("$select", matching(".*ID.*Name.*"))
+ .withQueryParam("$expand", equalTo("Category"))
+ .withQueryParam("$orderby", matching(".*Name.*asc.*"))
+ .withQueryParam("$top", equalTo("5"))
+ .withQueryParam("$skip", equalTo("10"))
+ .withQueryParam("$count", equalTo("true")));
+ }
+
+ @Test
+ void testProcessCreateEntity() throws Exception {
+ wireMock.stubFor(post(urlPathEqualTo("/odata/Products"))
+ .withRequestBody(equalToJson("{\"Name\":\"Tablet\",\"Price\":299.99}"))
+ .willReturn(aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":103,\"Name\":\"Tablet\",\"Price\":299.99}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.CREATE);
+
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("Name", "Tablet");
+ payload.put("Price", 299.99);
+ exchange.getMessage().setBody(payload);
+
+ producer.process(exchange);
+
+ assertEquals(201, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertEquals(new BigDecimal(103), body.get("ID"));
+ }
+
+ @Test
+ void testProcessIgnoresInboundHttpRoutingHeaders() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange = endpoint.createExchange();
+
+ // Simulate HTTP/REST headers inherited from an upstream consumer.
+ exchange.getMessage().setHeader(Exchange.HTTP_URI, "http://malicious.example/other");
+ exchange.getMessage().setHeader(Exchange.HTTP_PATH, "/wrong/path");
+ exchange.getMessage().setHeader(Exchange.HTTP_QUERY, "unexpected=true");
+ exchange.getMessage().setHeader(Exchange.HTTP_RAW_QUERY, "rawUnexpected=true");
+ exchange.getMessage().setHeader("CamelRestHttpUri", "http://malicious.example/rest");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products")));
+ }
+
+ @Test
+ void testProcessUpdateEntity() throws Exception {
+ ODataEndpoint keyEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer keyProducer = keyEndpoint.createProducer();
+ keyProducer.start();
+
+ try {
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withRequestBody(equalToJson("{\"Name\":\"Updated Laptop\"}"))
+ .willReturn(aResponse().withStatus(204)));
+
+ Exchange exchange = keyEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.UPDATE);
+
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("Name", "Updated Laptop");
+ exchange.getMessage().setBody(payload);
+
+ keyProducer.process(exchange);
+
+ assertEquals(204, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ } finally {
+ keyProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessDeleteEntity() throws Exception {
+ ODataEndpoint keyEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer keyProducer = keyEndpoint.createProducer();
+ keyProducer.start();
+
+ try {
+ wireMock.stubFor(delete(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse().withStatus(204)));
+
+ Exchange exchange = keyEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.DELETE);
+
+ keyProducer.process(exchange);
+
+ assertEquals(204, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNull(exchange.getMessage().getBody());
+ } finally {
+ keyProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithETagHeader() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("\"etag-v1\""))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":101,\"Name\":\"Laptop\",\"@odata.etag\":\"etag-v2\"}")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.ETAG, "\"etag-v1\"");
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("etag-v2", exchange.getMessage().getHeader(ODataConstants.ETAG));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithHttpETag() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(102)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(102)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withHeader("ETag", "W/\"etag-v3\"")
+ .withBody("{\"ID\":102,\"Name\":\"Monitor\"}")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("W/\"etag-v3--gzip\"", exchange.getMessage().getHeader(ODataConstants.ETAG));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessUpdateEntityWithETag() throws Exception {
+ ODataEndpoint updateEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer updateProducer = updateEndpoint.createProducer();
+ updateProducer.start();
+
+ try {
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"etag-v3\""))
+ .withHeader("Content-Type", containing("application/json"))
+ .withRequestBody(equalToJson("{\"Name\":\"Updated Laptop\"}"))
+ .willReturn(aResponse()
+ .withStatus(204)
+ .withHeader("ETag", "W/\"etag-v4\"")));
+
+ Exchange exchange = updateEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.UPDATE);
+ exchange.getMessage().setHeader(ODataConstants.ETAG, "W/\"etag-v3\"");
+ exchange.getMessage().setBody("{\"Name\":\"Updated Laptop\"}");
+
+ updateProducer.process(exchange);
+
+ assertEquals(204, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("W/\"etag-v4\"", exchange.getMessage().getHeader(ODataConstants.ETAG));
+
+ wireMock.verify(patchRequestedFor(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"etag-v3\"")));
+ } finally {
+ updateProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessUpdateEntityWithETagPreconditionFailed() throws Exception {
+ ODataEndpoint updateEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer updateProducer = updateEndpoint.createProducer();
+ updateProducer.start();
+
+ try {
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"stale-etag\""))
+ .willReturn(aResponse()
+ .withStatus(412)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "error": {
+ "code": "412",
+ "message": "The ETag does not match."
+ }
+ }
+ """)));
+
+ Exchange exchange = updateEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.UPDATE);
+ exchange.getMessage().setHeader(ODataConstants.ETAG, "W/\"stale-etag\"");
+ exchange.getMessage().setBody("{\"Name\":\"Updated Laptop\"}");
+
+ HttpOperationFailedException exception = assertThrows(
+ HttpOperationFailedException.class,
+ () -> updateProducer.process(exchange));
+
+ assertEquals(412, exception.getStatusCode());
+ wireMock.verify(patchRequestedFor(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"stale-etag\"")));
+ } finally {
+ updateProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadSetWithEncodedFilter() throws Exception {
+ ODataEndpoint readEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer readProducer = readEndpoint.createProducer();
+ readProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo("Name eq 'Laptop Pro'"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "value": [
+ {"ID": 101, "Name": "Laptop Pro"}
+ ]
+ }
+ """)));
+
+ Exchange exchange = readEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_SET);
+ exchange.getMessage().setHeader(ODataConstants.FILTER, "Name eq 'Laptop Pro'");
+
+ readProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo("Name eq 'Laptop Pro'")));
+ } finally {
+ readProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadSetWithComplexEncodedFilter() throws Exception {
+ ODataEndpoint readEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer readProducer = readEndpoint.createProducer();
+ readProducer.start();
+
+ String filter = "Name eq 'O''Reilly' and Category eq 'Office/Supplies' "
+ + "and Price gt 10.5 and (Status eq 'Active' or Code eq 'A%20B') "
+ + "and startswith(Name,'Pro')";
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo(filter))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "value": [
+ {
+ "ID": 101,
+ "Name": "O'Reilly Pro",
+ "Category": "Office/Supplies"
+ }
+ ]
+ }
+ """)));
+
+ Exchange exchange = readEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_SET);
+ exchange.getMessage().setHeader(ODataConstants.FILTER, filter);
+
+ readProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo(filter)));
+ } finally {
+ readProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadEntryWithTrailingSlashAndKey() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products/",
+ ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "ID": 101,
+ "Name": "Laptop"
+ }
+ """)));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "101");
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessNextLinkHeader() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[],\"@odata.nextLink\":\"http://localhost/odata/Products?$skip=10\"}")));
+
+ Exchange exchange = endpoint.createExchange();
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("http://localhost/odata/Products?$skip=10", exchange.getMessage().getHeader(ODataConstants.NEXT_LINK));
+ }
+
+ @Test
+ void testProcessEndpointConfigurationDefaults() throws Exception {
+ ODataEndpoint configuredEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?top=5&skip=10&filter=Name eq 'Phone'",
+ ODataEndpoint.class);
+ Producer configuredProducer = configuredEndpoint.createProducer();
+ configuredProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$top", equalTo("5"))
+ .withQueryParam("$skip", equalTo("10"))
+ .withQueryParam("$filter", matching(".*Name.*Phone.*"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[{\"ID\":102,\"Name\":\"Phone\"}]}")));
+
+ Exchange exchange = configuredEndpoint.createExchange();
+ configuredProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ } finally {
+ configuredProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessHeaderOverridesEndpointConfiguration() throws Exception {
+ ODataEndpoint configuredEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?top=5",
+ ODataEndpoint.class);
+ Producer configuredProducer = configuredEndpoint.createProducer();
+ configuredProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$top", equalTo("20"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange = configuredEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.TOP, 20);
+
+ configuredProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ } finally {
+ configuredProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessHttpErrorPropagatesException() {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse().withStatus(500)));
+
+ Exchange exchange = endpoint.createExchange();
+
+ assertThrows(HttpOperationFailedException.class, () -> producer.process(exchange));
+ }
+
+ @Test
+ void testProcessEmptyResponseBodyReturnsNullBody() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNull(exchange.getMessage().getBody());
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProducerSingletonAndLifecycle() {
+ assertTrue(producer.isSingleton());
+ }
+
+ @Test
+ void testBasicAuthentication() throws Exception {
+ String username = "adminUser";
+ String password = "adminPassword123";
+ String expectedHeader
+ = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
+
+ ODataEndpoint authEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authMethod=Basic&authUsername=" + username
+ + "&authPassword=" + password,
+ ODataEndpoint.class);
+ Producer authProducer = authEndpoint.createProducer();
+ authProducer.start();
+
+ try {
+ // Priority 1: Match and return 200 OK when correct credentials are present
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(1)
+ .withHeader("Authorization", equalTo(expectedHeader))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ // Priority 10: Fallback HTTP Basic 401 challenge with WWW-Authenticate header
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(10)
+ .willReturn(aResponse()
+ .withStatus(401)
+ .withHeader("WWW-Authenticate", "Basic realm=\"OData Test Realm\"")));
+
+ Exchange exchange = authEndpoint.createExchange();
+ authProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(expectedHeader)));
+ } finally {
+ authProducer.stop();
+ }
+ }
+
+ @Test
+ void testBearerAuthenticationAndExchangeHeaderPrecedence() throws Exception {
+ String configuredToken = "configuredBearerToken123";
+ String explicitHeaderToken = "Bearer explicitHeaderToken456";
+
+ ODataEndpoint authEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authBearerToken=" + configuredToken,
+ ODataEndpoint.class);
+ Producer authProducer = authEndpoint.createProducer();
+ authProducer.start();
+
+ try {
+ // Case 1: Configured bearer token attached automatically
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + configuredToken))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange1 = authEndpoint.createExchange();
+ authProducer.process(exchange1);
+ assertEquals(200, exchange1.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + configuredToken)));
+
+ // Case 2: Explicit Exchange Authorization header takes precedence over configured token
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(explicitHeaderToken))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange2 = authEndpoint.createExchange();
+ exchange2.getMessage().setHeader("Authorization", explicitHeaderToken);
+ authProducer.process(exchange2);
+ assertEquals(200, exchange2.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(explicitHeaderToken)));
+ } finally {
+ authProducer.stop();
+ }
+ }
+
+ @Test
+ void testBearerEndpointConcurrentIsolation() throws Exception {
+ String token1 = "tokenUnit1";
+ String token2 = "tokenUnit2";
+
+ ODataEndpoint endpoint1 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?authBearerToken=" + token1,
+ ODataEndpoint.class);
+ ODataEndpoint endpoint2 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?authBearerToken=" + token2,
+ ODataEndpoint.class);
+
+ Producer producer1 = endpoint1.createProducer();
+ Producer producer2 = endpoint2.createProducer();
+
+ producer1.start();
+ producer2.start();
+
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ int threadCount = 10;
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ ConcurrentLinkedQueue<Throwable> failures = new ConcurrentLinkedQueue<>();
+
+ try {
+ for (int i = 0; i < threadCount; i++) {
+ final int index = i;
+ executor.submit(() -> {
+ Exchange exchange = null;
+ try {
+ Producer targetProducer = (index % 2 == 0) ? producer1 : producer2;
+ ODataEndpoint targetEndpoint = (index % 2 == 0) ? endpoint1 : endpoint2;
+ exchange = targetEndpoint.createExchange();
+ targetProducer.process(exchange);
+ if (exchange.getException() != null) {
+ failures.add(exchange.getException());
+ }
+ } catch (Throwable t) {
+ failures.add(t);
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ assertTrue(failures.isEmpty(), "Worker threads encountered exceptions: " + failures);
+
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + token1)));
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + token2)));
+ } finally {
+ executor.shutdownNow();
+ producer1.stop();
+ producer2.stop();
+ }
+ }
+
+ @Test
+ void testBasicAuthConcurrentIsolation() throws Exception {
+ String user1 = "user1";
+ String pass1 = "pass1";
+ String expectedHeader1
+ = "Basic " + Base64.getEncoder().encodeToString((user1 + ":" + pass1).getBytes(StandardCharsets.UTF_8));
+
+ String user2 = "user2";
+ String pass2 = "pass2";
+ String expectedHeader2
+ = "Basic " + Base64.getEncoder().encodeToString((user2 + ":" + pass2).getBytes(StandardCharsets.UTF_8));
+
+ ODataEndpoint endpoint1 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authMethod=Basic&authUsername=" + user1
+ + "&authPassword=" + pass1,
+ ODataEndpoint.class);
+ ODataEndpoint endpoint2 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authMethod=Basic&authUsername=" + user2
+ + "&authPassword=" + pass2,
+ ODataEndpoint.class);
+
+ Producer producer1 = endpoint1.createProducer();
+ Producer producer2 = endpoint2.createProducer();
+
+ producer1.start();
+ producer2.start();
+
+ int threadCount = 10;
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+
+ try {
+ // Priority 1: Match and return 200 OK when user1 credentials are present
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(1)
+ .withHeader("Authorization", equalTo(expectedHeader1))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ // Priority 1: Match and return 200 OK when user2 credentials are present
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(1)
+ .withHeader("Authorization", equalTo(expectedHeader2))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ // Priority 10: Fallback HTTP Basic 401 challenge with WWW-Authenticate header
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(10)
+ .willReturn(aResponse()
+ .withStatus(401)
+ .withHeader("WWW-Authenticate", "Basic realm=\"OData Test Realm\"")));
+
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ ConcurrentLinkedQueue<Throwable> failures = new ConcurrentLinkedQueue<>();
+
+ for (int i = 0; i < threadCount; i++) {
+ final int index = i;
+ executor.submit(() -> {
+ Exchange exchange = null;
+ try {
+ Producer targetProducer = (index % 2 == 0) ? producer1 : producer2;
+ ODataEndpoint targetEndpoint = (index % 2 == 0) ? endpoint1 : endpoint2;
+ exchange = targetEndpoint.createExchange();
+ targetProducer.process(exchange);
+ if (exchange.getException() != null) {
+ failures.add(exchange.getException());
+ }
+ } catch (Throwable t) {
+ failures.add(t);
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ assertTrue(failures.isEmpty(), "Worker threads encountered exceptions: " + failures);
+
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(expectedHeader1)));
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(expectedHeader2)));
+ } finally {
+ executor.shutdownNow();
+ producer1.stop();
+ producer2.stop();
+ }
+ }
+
+ @Test
+ void testQueryEncodingWithAmpersand() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*John.*Sons.*"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.FILTER, "Name eq 'John & Sons'");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*John.*Sons.*")));
+ }
+
+ @Test
+ void testProcessReadEntryWithStringKey() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer producer = endpoint.createProducer();
+ producer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products('ABC-123')"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {"ID":"ABC-123","Name":"Laptop"}
+ """)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "'ABC-123'");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products('ABC-123')")));
+ } finally {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadEntryWithCompositeKey() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer producer = endpoint.createProducer();
+ producer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo(
+ "/odata/Products(ProductID=101,CategoryID=5)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {"ProductID":101,"CategoryID":5,"Name":"Laptop"}
+ """)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(
+ ODataConstants.KEY, "ProductID=101,CategoryID=5");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo(
+ "/odata/Products(ProductID=101,CategoryID=5)")));
+ } finally {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessWithConfiguredBasicAuthentication() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+
+ endpoint.getConfiguration().setAuthMethod("Basic");
+ endpoint.getConfiguration().setAuthUsername("odata-user");
+ endpoint.getConfiguration().setAuthPassword("odata-password");
+
+ Producer producer = endpoint.createProducer();
+ producer.start();
+
+ try {
+ String credentials = Base64.getEncoder()
+ .encodeToString("odata-user:odata-password".getBytes(StandardCharsets.UTF_8));
+
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Basic " + credentials))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "value": []
+ }
+ """)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_SET);
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Basic " + credentials)));
+ } finally {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessHttpsWithSslContextParameters() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ KeyStoreParameters trustStore = new KeyStoreParameters();
+ trustStore.setResource("localhost.p12");
+ trustStore.setPassword("changeit");
+ trustStore.setType("PKCS12");
+
+ TrustManagersParameters trustManagers = new TrustManagersParameters();
+ trustManagers.setKeyStore(trustStore);
+
+ SSLContextParameters sslContextParameters = new SSLContextParameters();
+ sslContextParameters.setTrustManagers(trustManagers);
+
+ String baseUri = "odata:https://localhost:" + wireMock.getHttpsPort() + "/odata/Products";
+
+ // Use different OData endpoint URIs while keeping the underlying HTTP URI identical.
+ ODataEndpoint httpsEndpoint = context.getEndpoint(
+ baseUri + "?useGlobalSslContextParameters=true", ODataEndpoint.class);
+ httpsEndpoint.setSslContextParameters(sslContextParameters);
+
+ ODataEndpoint endpointWithoutSsl = context.getEndpoint(
+ baseUri + "?useGlobalSslContextParameters=false", ODataEndpoint.class);
+
+ Producer httpsProducer = httpsEndpoint.createProducer();
+ Producer endpointWithoutSslProducer = endpointWithoutSsl.createProducer();
+
+ try {
+ httpsProducer.start();
+
+ Exchange exchange = httpsEndpoint.createExchange();
+ httpsProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNotNull(exchange.getMessage().getBody());
+
+ endpointWithoutSslProducer.start();
+
+ Exchange exchangeWithoutSsl = endpointWithoutSsl.createExchange();
+
+ Throwable thrown = assertThrows(Throwable.class, () -> endpointWithoutSslProducer.process(exchangeWithoutSsl));
+
+ // Check for SSLException / SSLHandshakeException in the cause chain
+ boolean hasSslCause = false;
+ Throwable current = thrown;
+ while (current != null) {
+ if (current instanceof javax.net.ssl.SSLException) {
+ hasSslCause = true;
+ break;
+ }
+ current = current.getCause();
+ }
+
+ assertTrue(hasSslCause, "Expected SSL failure cause, but received: " + thrown);
Review Comment:
**Nit (optional, non-blocking):** the project avoids fully qualified class names (test code included), and Camel already has a helper that walks the cause chain. This block could become:
```java
Throwable thrown = assertThrows(Throwable.class, () -> endpointWithoutSslProducer.process(exchangeWithoutSsl));
assertNotNull(ObjectHelper.getException(SSLException.class, thrown),
"Expected SSL failure cause, but received: " + thrown);
```
with `import javax.net.ssl.SSLException;` and `import org.apache.camel.util.ObjectHelper;` added.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154137261
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataHelper.java:
##########
@@ -0,0 +1,67 @@
+/*
+ * 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.odata;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.util.json.DeserializationException;
+import org.apache.camel.util.json.Jsoner;
+
+public final class ODataHelper {
+
+ private ODataHelper() {
+ }
+
+ public static String toJson(Object value) {
+ if (value == null) {
+ return null;
+ }
+
+ if (value instanceof String string) {
+ return string;
+ }
+
+ return Jsoner.serialize(value);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Map<String, Object> parseJsonObject(String value) throws DeserializationException {
+ if (value == null || value.isBlank()) {
+ return Map.of();
+ }
+
+ Object parsed = Jsoner.deserialize(value);
+ if (!(parsed instanceof Map)) {
+ throw new IllegalArgumentException("Expected an OData JSON object response");
+ }
+
+ return (Map<String, Object>) parsed;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static List<Map<String, Object>> extractValueList(Map<String, Object> response) {
+ Object value = response.get("value");
+
+ if (!(value instanceof List)) {
+ throw new IllegalArgumentException("OData response does not contain a value array");
+ }
+
+ return (List<Map<String, Object>>) value;
+ }
Review Comment:
Addressed in the latest commit `4cb8cea36f16`.
`extractValueList()` was unused, so it has been removed from `ODataHelper` rather than adding unnecessary usage to `ODataProducer`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154141305
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataComponent.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.odata;
+
+import java.net.URI;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.camel.Endpoint;
+import org.apache.camel.spi.annotations.Component;
+import org.apache.camel.support.DefaultComponent;
+
+@Component("odata")
+public class ODataComponent extends DefaultComponent {
+
+ @Override
+ protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters)
+ throws Exception {
+
+ ODataEndpoint endpoint = new ODataEndpoint(uri, this);
+
+ endpoint.setHttpUri(new URI(remaining));
+
+ // Create a copy of parameters before setProperties consumes known properties
+ Map<String, Object> customParams = new LinkedHashMap<>(parameters);
Review Comment:
Addressed in the latest commit `4cb8cea36f16`.
Removed the unused `customParams` copy from `ODataComponent`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4156240379
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,408 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData V4 services using standard OData CRUD operations.
+
+The component provides OData-specific request and response handling while using Camel HTTP as the underlying HTTP transport.
+
+The OData component is the successor to the deprecated `camel-olingo4` component.
+
+The component supports OData V4 services using JSON representations.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+[source,xml]
+
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-odata</artifactId>
+ <version>x.x.x</version>
+ <!-- use the same version as your Camel core version -->
+</dependency>
+----
Review Comment:
Addressed in commit c1a11055ded4.
Added the required opening and closing ---- delimiters to all [source,*] listing blocks in the OData component documentation and verified the file with git diff --check.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4164819027
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,359 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData (Open Data Protocol) services using standard OData CRUD operations.
+
+The component delegates HTTP transport to Camel HTTP while providing OData-specific operation, key, query option, authentication, and response handling.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+## [source,xml]
Review Comment:
Done in #27275 (examples as Java, XML and YAML tabs).
_Claude Code on behalf of davsclaus_
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4164728456
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,414 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData V4 services using standard OData CRUD operations.
+
+The component provides OData-specific request and response handling while using Camel HTTP as the underlying HTTP transport.
+
+The OData component is the successor to the deprecated `camel-olingo4` component.
+
+The component supports OData V4 services using JSON representations.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+[source,xml]
+----
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-odata</artifactId>
+ <version>x.x.x</version>
+ <!-- use the same version as your Camel core version -->
+</dependency>
+----
+
+== URI Format
+
+=== odata:httpUri
+
+Where `httpUri` is the base URL of the OData entity set or resource.
+
+For example:
+
+[source,text]
+----
+odata:http://localhost:8080/odata/Products
+odata:https://services.odata.org/V4/Northwind/Northwind.svc/Categories
+----
+
+The URI can also contain endpoint options as query parameters.
+
+The OData query options can alternatively be configured on the endpoint or supplied dynamically using message headers.
+
+The component uses Camel HTTP for the underlying HTTP transport.
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+[NOTE]
+====
+This is a producer-only component. Per-message options can be supplied through `CamelOData` headers in addition to endpoint options.
+====
+
+[NOTE]
+====
+If the route receives messages from an untrusted producer, take care when forwarding internal Camel headers to this component. In particular, an inbound `Authorization` header can be used for the OData request and takes precedence over endpoint authentication.
+====
+
+== Operations
+
+The operation is selected using the `CamelODataOperation` message header or the `operation` endpoint option.
+
+The following operations are supported:
+
+[cols="20%,15%,65%",options="header"]
+|===
+| Operation | HTTP Method | Description
+
+| `READ_SET`
+| `GET`
+| Reads a collection of entities. This is the default operation.
+
+| `READ_ENTRY`
+| `GET`
+| Reads a single entity. Use `CamelODataKey` to specify the entity key.
+
+| `CREATE`
+| `POST`
+| Creates a new entity using the message body as the request payload.
+
+| `UPDATE`
+| `PATCH`
+| Updates an existing entity using `CamelODataKey` to identify the entity.
+
+| `DELETE`
+| `DELETE`
+| Deletes an existing entity using `CamelODataKey` to identify the entity.
+|===
+
+For `READ_ENTRY`, `UPDATE`, and `DELETE`, the key is appended to the configured resource URI.
+
+For example:
+
+[source,text]
+----
+CamelODataKey = 101
+odata:http://localhost:8080/odata/Products
+----
+
+results in:
+
+[source,text]
+----
+http://localhost:8080/odata/Products(101)
+----
+
+The key is treated as an OData key expression. The component does not automatically add quotes or otherwise change the supplied key value.
+
+For example, a string key can be supplied as `'ABC-123'`, while a composite key can be supplied as `ProductID=101,CategoryID=5`.
+
+The key value should be a valid and trusted OData key expression.
+
+== Message Headers
+
+The following message headers can be used to control OData processing dynamically.
+
+[cols="25%,20%,55%",options="header"]
+|===
+| Header | Type | Description
+
+| `CamelODataOperation`
+| `ODataOperation` / `String`
+| Specifies the operation to execute.
+
+| `CamelODataKey`
+| `String`
+| Specifies the key for `READ_ENTRY`, `UPDATE`, and `DELETE` operations.
+
+| `CamelODataFilter`
+| `String`
+| Overrides the `$filter` query option.
+
+| `CamelODataSelect`
+| `String`
+| Overrides the `$select` query option.
+
+| `CamelODataExpand`
+| `String`
+| Overrides the `$expand` query option.
+
+| `CamelODataOrderBy`
+| `String`
+| Overrides the `$orderby` query option.
+
+| `CamelODataTop`
+| `Integer`
+| Overrides the `$top` query option.
+
+| `CamelODataSkip`
+| `Integer`
+| Overrides the `$skip` query option.
+
+| `CamelODataIncludeCount`
+| `Boolean`
+| Controls whether the `$count` query option is included in the request.
+
+| `CamelODataETag`
+| `String`
+| Supplies an ETag for the `If-Match` HTTP request header. The component also populates this header from an ETag returned by the OData service.
+
+| `CamelODataCount`
+| `Long`
+| Contains the count returned by the OData service when an `@odata.count` value is present.
+
+| `CamelODataNextLink`
+| `String`
+| Contains the `@odata.nextLink` value returned by the OData service when present.
+
+| `CamelHttpResponseCode`
+| `Integer`
+| Contains the HTTP response status code returned by the OData service.
+|===
+
+A message-level `Authorization` header can also be supplied.
+
+When an `Authorization` header is present, it takes precedence over authentication configured on the OData endpoint.
+
+If the OData route is exposed to an upstream HTTP consumer, such as `platform-http`, take care that an inbound `Authorization` header may be forwarded to the OData service.
+
+If the upstream authorization credentials must not be forwarded, remove the header before sending the exchange to the OData component.
+
+For example:
+
+[source,java]
+----
+from("platform-http:/products")
+.removeHeader("Authorization")
+.to("odata:http://my-odata-service/odata/Products");
+----
+
+== Query Options
+
+The component supports the following OData query options:
+
+[cols="25%,20%,55%",options="header"]
+|===
+| Option | Type | Description
+
+| `filter`
+| `String`
+| OData `$filter` expression.
+
+| `select`
+| `String`
+| OData `$select` expression.
+
+| `expand`
+| `String`
+| OData `$expand` expression.
+
+| `orderBy`
+| `String`
+| OData `$orderby` expression.
+
+| `top`
+| `Integer`
+| OData `$top` value.
+
+| `skip`
+| `Integer`
+| OData `$skip` value.
+
+| `count`
+| `Boolean`
+| Controls the OData `$count` query option.
+|===
+
+Additional OData query options can be supplied as endpoint URI parameters. Because the component uses lenient endpoint properties, options that are not defined as explicit component options are passed through as OData query parameters. Ensure that endpoint option names are spelled correctly and that any additional options are valid OData query options.
+
+For example:
+
+[source,text]
+----
+odata:http://localhost:8080/odata/Products?filter=Name%20eq%20%27Laptop%27&select=Name,Price&top=10
+----
+
+Query options configured through message headers take precedence over the corresponding endpoint configuration.
+
+== Authentication
+
+The component supports Basic authentication and Bearer token authentication through endpoint options.
+
+For example, Basic authentication can be configured using:
+
+[source,text]
+----
+odata:http://localhost:8080/odata/Products?authMethod=Basic&authUsername=user&authPassword=password
+----
+
+Bearer token authentication can be configured using the `authBearerToken` option.
+
+Authentication can also be supplied dynamically using the message-level `Authorization` header:
+
+[source,java]
+----
+from("direct:getProducts")
+.setHeader("Authorization", constant("Bearer my-token"))
+.to("odata:http://my-odata-service/odata/Products");
+----
Review Comment:
yeah the AI now have tools to do this for us, so we just ask it to do it afterwards as a chore PR
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "apupier (via GitHub)" <gi...@apache.org>.
apupier commented on PR #27168:
URL: https://github.com/apache/camel/pull/27168#issuecomment-5928169203
doc validation problem:
```
[45](https://github.com/apache/camel/actions/runs/36819719619/job/110232461000?pr=27168#step:4:151)
[05:29:23.162] WARN (asciidoctor): unterminated listing block
file: /home/runner/work/camel/camel/camel/docs/components/modules/ROOT/pages/odata-component.adoc:29
source: /home/runner/work/camel/camel/camel (branch: HEAD <worktree> | start path: docs/components)
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "github-actions[bot] (via GitHub)" <gi...@apache.org>.
github-actions[bot] commented on PR #27168:
URL: https://github.com/apache/camel/pull/27168#issuecomment-5925512199
<!-- ci-tested-modules -->
:test_tube: **CI tested the following changed modules:**
- `bom/camel-bom`
- `catalog/camel-allcomponents`
- `catalog/camel-catalog`
- `components`
- `components/camel-odata`
- `core/camel-main`
- `core/camel-util`
- `docs`
- `dsl/camel-componentdsl`
- `dsl/camel-endpointdsl`
- `dsl/camel-kamelet-main`
- `parent`
> :information_source: Dependent modules were not tested because the total number of affected modules exceeded the threshold (50). Use the `test-dependents` label to force testing all dependents.
---
<details><summary>:microscope: Scalpel shadow comparison — Scalpel: 578 of 698 tested, 0 compile-only — current: 576 all tested</summary>
[Maveniverse Scalpel](https://github.com/maveniverse/scalpel) detected **578 affected modules** (current approach: 576).
**Skip-tests mode would test 578 modules** (17 direct + 28 downstream), **skip tests for 0** (generated code, meta-modules)
<details><summary>:warning: Modules only in Scalpel (2)</summary>
- `camel-bom`
- `camel-etc`
</details>
<details><summary>Modules Scalpel would test (578)</summary>
- `archetypes` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-a2a` ← depends on affected reactor module org.apache.camel:camel-platform-http-vertx
- `camel-activemq` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-activemq6` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-ai-observability` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ai-observability-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ai-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-ai-resource` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ai-tool` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-common` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-alibaba-eventbridge` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-fc` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-kms` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-mns` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-oss` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-ots` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-alibaba-sls` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-alibaba-sms` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-amqp` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-api` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-api-component-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-arangodb` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-archetype-api-component` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-archetype-component` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-archetype-dataformat` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-archetype-java` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-archetype-main` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-as2` ← depends on affected reactor module org.apache.camel:camel-jetty
- `camel-as2-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-as2-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-asn1` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-asterisk` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-atmosphere-websocket` ← depends on affected reactor module org.apache.camel:camel-test-infra-jetty
- `camel-atom` ← depends on affected reactor module org.apache.camel:camel-test-infra-jetty
- `camel-attachments` ← depends on affected reactor module org.apache.camel:camel-bean
- `camel-avro` ← depends on affected reactor module org.apache.camel:camel-spring-xml
- `camel-avro-rpc` ← depends on affected reactor module org.apache.camel:camel-spring-xml
- `camel-avro-rpc-jetty` ← depends on affected reactor module org.apache.camel:camel-api
- `camel-avro-rpc-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-avro-rpc-spi` ← downstream of org.apache.camel:camel-core-model
- `camel-aws-bedrock` ← depends on affected reactor module org.apache.camel:camel-base64
- `camel-aws-cloudtrail` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws-common` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-aws-config` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-aws-parameter-store` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-aws-secrets-manager` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws-security-hub` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-athena` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-comprehend` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-cw` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-ddb` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-ec2` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-ecs` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-eks` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-eventbridge` ← depends on affected reactor module org.apache.camel:camel-aws2-sqs
- `camel-aws2-iam` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-kinesis` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-aws2-kms` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-aws2-lambda` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-mq` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-msk` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-polly` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-redshift` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-rekognition` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-s3` ← depends on affected reactor module org.apache.camel:camel-zip-deflater
- `camel-aws2-s3-vectors` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-ses` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-sns` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-sqs` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-step-functions` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-sts` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-aws2-textract` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-timestream` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-aws2-transcribe` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-aws2-translate` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-azure-common` ← downstream of org.apache.camel:camel-core-model
- `camel-azure-cosmosdb` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-eventgrid` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-eventhubs` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-files` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-functions` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-azure-key-vault` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-azure-schema-registry` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-azure-servicebus` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-azure-storage-blob` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-storage-datalake` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-azure-storage-queue` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-barcode` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-base` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-base-engine` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-base64` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-bean` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-bean-validator` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-beanio` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-bindy` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-bom` ← own pom bom/camel-bom/pom.xml changed
- `camel-bonita` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-box` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-box-api` ← depends on affected reactor module org.apache.camel:camel-api
- `camel-box-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-braintree` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-browse` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-caffeine` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-camunda` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cassandraql` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-catalog-common` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-cbor` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-chatscript` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-chunk` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cli-connector` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cli-debug` ← depends on affected reactor module org.apache.camel:camel-cli-connector
- `camel-clickhouse` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-clickup` ← depends on affected reactor module org.apache.camel:camel-netty-http
- `camel-cloudevents` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cluster` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-cm-sms` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-coap` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cometd` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-console` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-consul` ← depends on affected reactor module org.apache.camel:camel-spring-main
- `camel-controlbus` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-core` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-core-all` ← depends on affected reactor module org.apache.camel:camel-xml-jaxp
- `camel-core-catalog` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-core-engine` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-core-languages` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-core-model` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-core-processor` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-core-reifier` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-core-xml` ← depends on affected reactor module org.apache.camel:camel-core-engine
- `camel-couchbase` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-couchdb` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cron` ← depends on affected reactor module org.apache.camel:camel-timer
- `camel-crypto` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-crypto-pgp` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-csv` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-cxf-common` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cxf-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-cxf-rest` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-cxf-soap` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-cxf-spring-common` ← depends on affected reactor module org.apache.camel:camel-http-base
- `camel-cxf-spring-rest` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-cxf-spring-soap` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-cxf-spring-transport` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cxf-transport` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-cyberark-vault` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dapr` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dataformat` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-dataset` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-datasonnet` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-dataweave` ← downstream of org.apache.camel:camel-core-model
- `camel-debezium-common` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-debezium-common-parent` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-debezium-db2` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-debezium-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-debezium-mongodb` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-debezium-mysql` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-debezium-oracle` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-debezium-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-debezium-postgres` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-debezium-sqlserver` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-debug` ← depends on affected reactor module org.apache.camel:camel-xml-io
- `camel-dependencies` ← downstream of org.apache.camel:sync-properties-maven-plugin
- `camel-dfdl` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dhis2` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dhis2-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dhis2-parent` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-diagram` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-direct` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-disruptor` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-djl` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dns` ← depends on affected reactor module org.apache.camel:camel-log
- `camel-docker` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-docling` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-drill` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dropbox` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dsl-modeline` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dsl-support` ← depends on affected reactor module org.apache.camel:camel-core-model
- `camel-duckdb` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-dynamic-router` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-ehcache` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-eip-documentation-enricher-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-elasticsearch` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-elasticsearch-rest-client` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-etc` ← effective property workspace
- `camel-event` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-exec` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-fastjson` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-fhir` ← depends on affected reactor module org.apache.camel:camel-hl7
- `camel-fhir-api` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-fhir-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-file` ← depends on affected reactor module org.apache.camel:camel-core-engine
- `camel-file-watch` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-flatpack` ← depends on affected reactor module org.apache.camel:camel-direct
- `camel-flink` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-flowable` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-fop` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-fory` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-freemarker` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-ftp` ← depends on affected reactor module org.apache.camel:camel-spring-xml
- `camel-ftp-common` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-geocoder` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-git` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-github2` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-bigquery` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-calendar` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-common` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-google-drive` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-firestore` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-functions` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-mail` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-google-pubsub` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-secret-manager` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-sheets` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-google-speech-to-text` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-storage` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-text-to-speech` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-vertexai` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-google-vision` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-graphql` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-grok` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-groovy` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-grpc` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-gson` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-hashicorp-vault` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-hazelcast` ← depends on affected reactor module org.apache.camel:camel-spring-main
- `camel-health` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-hivemq` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-hl7` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-http` ← depends on affected reactor module org.apache.camel:camel-test-infra-jetty
- `camel-http-base` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-http-common` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-huawei-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-huaweicloud-common` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-huaweicloud-dms` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-huaweicloud-frs` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-huaweicloud-functiongraph` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-huaweicloud-iam` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-huaweicloud-imagerecognition` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-huaweicloud-obs` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-huaweicloud-smn` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-huggingface` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ibm-cos` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ibm-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-ibm-secrets-manager` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ibm-watson-discovery` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ibm-watson-language` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ibm-watson-speech-to-text` ← depends on affected reactor module org.apache.camel:camel-ibm-watson-text-to-speech
- `camel-ibm-watson-text-to-speech` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ibm-watsonx-ai` ← depends on affected reactor module org.apache.camel:camel-vertx-websocket
- `camel-ibm-watsonx-data` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ical` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-iggy` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-ignite` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-infinispan` ← depends on affected reactor module org.apache.camel:camel-stub
- `camel-infinispan-common` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-infinispan-embedded` ← depends on affected reactor module org.apache.camel:camel-log
- `camel-infinispan-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-influxdb` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-influxdb2` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-iso8583` ← depends on affected reactor module org.apache.camel:camel-groovy
- `camel-jackson` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jackson-avro` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jackson-protobuf` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jackson3` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jackson3-avro` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jackson3-protobuf` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jackson3xml` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jacksonxml` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jactl` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-jandex` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-jasypt` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-java-io` ← depends on affected reactor module org.apache.camel:camel-xml-io
- `camel-java-joor-dsl` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-javascript` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-jaxb` ← depends on affected reactor module org.apache.camel:camel-spring-xml
- `camel-jbang-console` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-jbang-mcp` ← depends on affected reactor module org.apache.camel:camel-xml-io-dsl
- `camel-jbang-plugin-mcp` ← depends on affected reactor module org.apache.camel:camel-jbang-core
- `camel-jbang-plugin-route-parser` ← depends on affected reactor module org.apache.camel:camel-route-parser
- `camel-jbang-plugin-tui` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jbang-plugin-validate` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jcache` ← depends on affected reactor module org.apache.camel:camel-core-languages
- `camel-jcr` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jdbc` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jetty` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-jetty-common` ← depends on affected reactor module org.apache.camel:camel-jaxb
- `camel-jfr` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-jgroups` ← depends on affected reactor module org.apache.camel:camel-master
- `camel-jgroups-raft` ← depends on affected reactor module org.apache.camel:camel-master
- `camel-jira` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jms` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-jmx` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jolt` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jooq` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-joor` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-jpa` ← effective property camel.surefire.fork.additional-vmargs
- `camel-jq` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-jsch` ← depends on affected reactor module org.apache.camel:camel-ssh
- `camel-jslt` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-json-validator` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jsonapi` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jsonata` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jsonb` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-jsonpath` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-jsoup` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-jt400` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-jta` ← depends on affected reactor module org.apache.camel:camel-sql
- `camel-jte` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-kafka` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-kamelet` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-kamelet-main-support` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-keycloak` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-knative` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-knative-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-knative-http` ← depends on affected reactor module org.apache.camel:camel-http
- `camel-knative-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-kserve` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-kubernetes` ← depends on affected reactor module org.apache.camel:camel-log
- `camel-kudu` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-langchain4j-agent` ← depends on affected reactor module org.apache.camel:camel-telemetry
- `camel-langchain4j-agent-api` ← depends on affected reactor module org.apache.camel:camel-api
- `camel-langchain4j-chat` ← depends on affected reactor module org.apache.camel:camel-telemetry
- `camel-langchain4j-core` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-langchain4j-embeddings` ← depends on affected reactor module org.apache.camel:camel-test-infra-qdrant
- `camel-langchain4j-embeddingstore` ← depends on affected reactor module org.apache.camel:camel-test-infra-qdrant
- `camel-langchain4j-embeddingstore-api` ← depends on affected reactor module org.apache.camel:camel-api
- `camel-langchain4j-ingest` ← depends on affected reactor module org.apache.camel:camel-test-infra-qdrant
- `camel-langchain4j-tokenizer` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-langchain4j-web-search` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-language` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-launcher-container` ← effective property docker.context
- `camel-ldap` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ldif` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-log` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-lra` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-lucene` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-lumberjack` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-lzf` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-mail` ← depends on affected reactor module org.apache.camel:camel-quartz
- `camel-mail-microsoft-oauth` ← depends on affected reactor module org.apache.camel:camel-mail
- `camel-main` ← core/camel-main/src/generated/resources/org/apache/camel/main/components.properties
- `camel-management` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-management-api` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-mapstruct` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-master` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-spring-main
- `camel-mcp-server` ← depends on affected reactor module org.apache.camel:camel-openai
- `camel-mcp-server-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-mdc` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-metrics` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-micrometer` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-micrometer-observability` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-micrometer-prometheus` ← depends on affected reactor module org.apache.camel:camel-platform-http-main
- `camel-microprofile-config` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-microprofile-fault-tolerance` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-microprofile-health` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-microprofile-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-milo` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-milvus` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-mina` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-mina-sftp` ← depends on affected reactor module org.apache.camel:camel-core-languages
- `camel-minio` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-mllp` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-mock` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-mongodb` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-mongodb-gridfs` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-mustache` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-mvel` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-mybatis` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-nats` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-neo4j` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-netty` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-netty-http` ← depends on affected reactor module org.apache.camel:camel-jetty
- `camel-oaipmh` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-oauth` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-observability-services` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-observation` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-ocsf` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-odata` ← components/camel-odata/src/generated/java/org/apache/camel/component/odata/ODataComponentConfigurer.java, components/camel-odata/src/generated/java/org/apache/camel/component/odata/ODataEndpointConfigurer.java, components/camel-odata/src/generated/java/org/apache/camel/component/odata/ODataEndpointUriFactory.java, components/camel-odata/src/generated/resources/META-INF/org/apache/camel/component/odata/odata.json, components/camel-odata/src/generated/resources/META-INF/services/org/apache/camel/component.properties, components/camel-odata/src/generated/resources/META-INF/services/org/apache/camel/component/odata, components/camel-odata/src/generated/resources/META-INF/services/org/apache/camel/configurer/odata-component, components/camel-odata/src/generated/resources/META-INF/services/org/apache/camel/configurer/odata-endpoint, components/camel-odata/src/generated/resources/META-INF/services/org/apache/camel/other.properties, components/camel-odata/src/generated
/resources/META-INF/services/org/apache/camel/urifactory/odata-endpoint, components/camel-odata/src/generated/resources/odata.json, components/camel-odata/src/main/docs/odata-component.adoc, components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataComponent.java, components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java, components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConstants.java, components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataEndpoint.java, components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataHelper.java, components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataOperation.java, components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java, components/camel-odata/src/main/resources/META-INF/services/org/apache/camel/component/odata, components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataC
omponentTest.java, components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataEndpointTest.java, components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java, own pom components/camel-odata/pom.xml changed
- `camel-ognl` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-olingo2` ← depends on affected reactor module org.apache.camel:camel-test-infra-jetty
- `camel-olingo2-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-olingo2-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-olingo4` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-olingo4-api` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-olingo4-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-once` ← depends on affected reactor module org.apache.camel:camel-groovy
- `camel-opa` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-openai` ← depends on affected reactor module org.apache.camel:camel-jackson
- `camel-openapi-java` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-openapi-rest-dsl-generator` ← depends on affected reactor module org.apache.camel:camel-core-engine
- `camel-openapi-validator` ← depends on affected reactor module org.apache.camel:camel-platform-http-vertx
- `camel-openfga` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-opensearch` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-openstack` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-opentelemetry` ← effective property opentracing-agent.lib
- `camel-opentelemetry-metrics` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-opentelemetry2` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-optaplanner` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-paho` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-paho-mqtt5` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-parquet-avro` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pdf` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pg-replication-slot` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pgevent` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pgvector` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pinecone` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-platform-http` ← depends on affected reactor module org.apache.camel:camel-http
- `camel-platform-http-jolokia` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-platform-http-main` ← depends on affected reactor module org.apache.camel:camel-openapi-java
- `camel-platform-http-vertx` ← depends on affected reactor module org.apache.camel:camel-vertx-http
- `camel-plc4x` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pqc` ← depends on affected reactor module org.apache.camel:camel-crypto
- `camel-printer` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-protobuf` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-pubnub` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-pulsar` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-python` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-python3` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-qdrant` ← depends on affected reactor module org.apache.camel:camel-test-infra-qdrant
- `camel-quartz` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-quickfix` ← depends on affected reactor module org.apache.camel:camel-jetty
- `camel-quickjs` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-reactive-streams` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-reactor` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-redis` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-ref` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-resilience4j` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-resilience4j-micrometer` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-resourceresolver-github` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-rest` ← depends on affected reactor module org.apache.camel:camel-core-languages
- `camel-rest-openapi` ← depends on affected reactor module org.apache.camel:camel-vertx-http
- `camel-rest-postman` ← depends on affected reactor module org.apache.camel:camel-vertx-http
- `camel-restdsl-openapi-plugin` ← depends on affected reactor module org.apache.camel:camel-jetty
- `camel-robotframework` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-rocketmq` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-rss` ← depends on affected reactor module org.apache.camel:camel-test-infra-jetty
- `camel-rxjava` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-saga` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-salesforce` ← depends on affected reactor module org.apache.camel:camel-core-catalog
- `camel-salesforce-codegen` ← depends on affected reactor module org.apache.camel:camel-salesforce
- `camel-salesforce-parent` ← effective property salesforce.component.root
- `camel-sap-netweaver` ← depends on affected reactor module org.apache.camel:camel-velocity
- `camel-saxon` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-scheduler` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-schematron` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-seda` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-semantic` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-servicenow` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-servicenow-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-servicenow-parent` ← effective property servicenow.component.root
- `camel-servlet` ← depends on affected reactor module org.apache.camel:camel-gson
- `camel-shell` ← depends on affected reactor module org.apache.camel:camel-test-junit5
- `camel-shiro` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-sjms` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-sjms2` ← depends on affected reactor module org.apache.camel:camel-test-infra-artemis
- `camel-slack` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-smb` ← depends on affected reactor module org.apache.camel:camel-test-infra-smb
- `camel-smooks` ← depends on affected reactor module org.apache.camel:camel-attachments
- `camel-smpp` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-snakeyaml` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-snmp` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-soap` ← depends on affected reactor module org.apache.camel:camel-cxf-soap
- `camel-solr` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-spiffe` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-splunk-hec` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-spring` ← depends on affected reactor module org.apache.camel:camel-cron
- `camel-spring-ai-chat` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-ai-embeddings` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-spring-ai-image` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-spring-ai-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-spring-ai-vector-store` ← depends on affected reactor module org.apache.camel:camel-test-infra-qdrant
- `camel-spring-batch` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-cloud-config` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-jdbc` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-ldap` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-main` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-spring-rabbitmq` ← depends on affected reactor module org.apache.camel:camel-http
- `camel-spring-redis` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-spring-security` ← depends on affected reactor module org.apache.camel:camel-core-xml
- `camel-spring-ws` ← depends on affected reactor module org.apache.camel:camel-jaxb
- `camel-spring-xml` ← depends on affected reactor module org.apache.camel:camel-cron
- `camel-sql` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-ssh` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-state-store` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-state-store-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-stax` ← depends on affected reactor module org.apache.camel:camel-jaxb
- `camel-stitch` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-stream` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-streamcaching-test` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-stringtemplate` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-stripe` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-stub` ← depends on affected reactor module org.apache.camel:camel-seda
- `camel-support` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-swift` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-syslog` ← depends on affected reactor module org.apache.camel:camel-mina
- `camel-tahu` ← depends on affected reactor module org.apache.camel:camel-test-infra-core
- `camel-tarfile` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-telegram` ← depends on affected reactor module org.apache.camel:camel-netty-http
- `camel-telemetry` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-telemetry-dev` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-tensorflow-serving` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-test-infra-all` ← depends on affected reactor module org.apache.camel:camel-test-infra-qdrant
- `camel-test-infra-artemis` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-test-infra-cli` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-test-infra-core` ← depends on affected reactor module org.apache.camel:camel-core
- `camel-test-infra-jetty` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-test-infra-qdrant` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-test-infra-smb` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-test-junit5` ← depends on affected reactor module org.apache.camel:camel-xml-jaxb
- `camel-test-junit6` ← depends on affected reactor module org.apache.camel:camel-xml-jaxb
- `camel-test-main-junit5` ← depends on affected reactor module org.apache.camel:camel-yaml-io
- `camel-test-main-junit6` ← depends on affected reactor module org.apache.camel:camel-yaml-io
- `camel-test-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-test-spring-junit5` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-test-spring-junit6` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-thrift` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-thymeleaf` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-tika` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-timer` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-tooling-maven` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-toon` ← depends on affected reactor module org.apache.camel:camel-test-junit5
- `camel-tracing` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-twilio` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-twitter` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-typesafe-ai` ← depends on affected reactor module org.apache.camel:camel-xml-io-dsl
- `camel-ubl` ← depends on affected reactor module org.apache.camel:camel-test-junit5
- `camel-undertow` ← depends on affected reactor module org.apache.camel:camel-openapi-java
- `camel-undertow-spring-security` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-univocity-parsers` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-util` ← core/camel-util/src/main/java/org/apache/camel/util/ArtifactUtils.java
- `camel-validator` ← depends on affected reactor module org.apache.camel:camel-xml-jaxp
- `camel-velocity` ← depends on affected reactor module org.apache.camel:camel-management
- `camel-vertx` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-vertx-common` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-vertx-http` ← depends on affected reactor module org.apache.camel:camel-undertow
- `camel-vertx-parent` ← downstream of org.apache.camel:camel-core-model
- `camel-vertx-websocket` ← depends on affected reactor module org.apache.camel:camel-main
- `camel-wal` ← depends on affected reactor module org.apache.camel:camel-support
- `camel-wasm` ← effective property camel.surefire.fork.additional-vmargs
- `camel-weather` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-weaviate` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-web3j` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-webhook` ← depends on affected reactor module org.apache.camel:camel-netty-http
- `camel-whatsapp` ← depends on affected reactor module org.apache.camel:camel-netty-http
- `camel-wordpress` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-workday` ← depends on affected reactor module org.apache.camel:camel-mock
- `camel-xchange` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xj` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xml-io` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-xml-io-dsl` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xml-jaxb` ← depends on affected reactor module org.apache.camel:camel-core-model
- `camel-xml-jaxb-dsl` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xml-jaxb-dsl-test-definition` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xml-jaxb-dsl-test-spring` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xml-jaxp` ← depends on affected reactor module org.apache.camel:camel-util
- `camel-xmlsecurity` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xmpp` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-xpath` ← depends on affected reactor module org.apache.camel:camel-xml-jaxp
- `camel-xslt` ← depends on affected reactor module org.apache.camel:camel-xml-jaxp
- `camel-xslt-saxon` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-yaml-dsl-common` ← depends on affected reactor module org.apache.camel:camel-core-engine
- `camel-yaml-dsl-validator` ← depends on affected reactor module org.apache.camel:camel-xml-io
- `camel-yaml-dsl-validator-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-yaml-dsl-validator
- `camel-yaml-io` ← depends on affected reactor module org.apache.camel:camel-xml-io
- `camel-zendesk` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-zip-deflater` ← depends on affected reactor module org.apache.camel:camel-test-junit6
- `camel-zipfile` ← depends on affected reactor module org.apache.camel:camel-test-spring-junit6
- `camel-zookeeper` ← depends on affected reactor module org.apache.camel:camel-master
- `camel-zookeeper-master` ← depends on affected reactor module org.apache.camel:camel-ftp
- `components` ← downstream of org.apache.camel:camel-core-model
- `sync-properties-maven-plugin` ← depends on affected reactor module org.apache.camel:camel-util
</details>
> :information_source: Shadow mode — Scalpel observes but does not affect test execution. [Learn more](https://github.com/maveniverse/scalpel)
</details>
:warning: **Some tests are disabled on GitHub Actions** (`@DisabledIfSystemProperty(named = "ci.env.name")`) and require manual verification:
- `components`: 93 test(s) disabled on GitHub Actions
<details><summary>Build reactor — dependencies compiled but only changed modules were tested (12 modules, 2m 47s total)</summary>
**Total reactor time:** 2m 47s
| Module | Duration | Status |
| --- | --- | --- |
| Camel :: Component DSL | 50.6s | SUCCESS |
| Camel :: Main | 37.6s | SUCCESS |
| Camel :: Catalog :: Camel Catalog | 24.2s | SUCCESS |
| Camel :: Docs | 16.2s | SUCCESS |
| Camel :: OData | 11.9s | SUCCESS |
| Camel :: Util | 9.6s | SUCCESS |
| Camel :: Kamelet Main | 7.9s | SUCCESS |
| Camel :: All Components Sync point | 4.7s | SUCCESS |
| Camel :: Components | 3.5s | SUCCESS |
| Camel :: Parent | 1.0s | SUCCESS |
| Camel :: BOM | 0.1s | SUCCESS |
| Camel :: Endpoint DSL | n/a | |
**Top 20 slowest modules:**
- `Camel :: Component DSL` (50.6s)
- `Camel :: Main` (37.6s)
- `Camel :: Catalog :: Camel Catalog` (24.2s)
- `Camel :: Docs` (16.2s)
- `Camel :: OData` (11.9s)
- `Camel :: Util` (9.6s)
- `Camel :: Kamelet Main` (7.9s)
- `Camel :: All Components Sync point` (4.7s)
- `Camel :: Components` (3.5s)
- `Camel :: Parent` (1.0s)
- `Camel :: BOM` (0.1s)
</details>
---
:gear: [View full build and test results](https://github.com/apache/camel/actions/runs/36819719494)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4152098913
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * 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.odata;
+
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriParams;
+import org.apache.camel.support.jsse.SSLContextParameters;
+
+@UriParams
+public class ODataConfiguration {
+
+ @UriParam(label = "producer", description = "The OData operation to perform")
+ private ODataOperation operation = ODataOperation.READ_SET;
+
+ @UriParam(label = "producer", description = "The OData $filter query parameter")
+ private String filter;
+
+ @UriParam(label = "producer", description = "The OData $select query parameter")
+ private String select;
+
+ @UriParam(label = "producer", description = "The OData $expand query parameter")
+ private String expand;
+
+ @UriParam(label = "producer", description = "The OData $orderby query parameter")
+ private String orderBy;
+
+ @UriParam(label = "producer", description = "The OData $top query parameter")
+ private Integer top;
+
+ @UriParam(label = "producer", description = "The OData $skip query parameter")
+ private Integer skip;
+
+ @UriParam(label = "producer", description = "The OData $count query parameter")
+ private Boolean count;
+
+ @UriParam(label = "security", description = "Authentication method to use (e.g., Basic, Bearer)")
+ private String authMethod;
+
+ @UriParam(label = "security", description = "Username for Basic authentication")
+ private String authUsername;
+
+ @UriParam(label = "security", description = "Password for Basic authentication")
+ private String authPassword;
Review Comment:
🔴 **Security:** `authPassword` is not annotated with `secret = true` on its `@UriParam`. This means the password will appear in plain text in endpoint URIs, logs, JMX, and management APIs. Same issue on `authBearerToken` at line 62.
The generated `odata.json` confirms this — both properties have `"secret": false` and `SECRET_PROPERTY_NAMES` is empty.
```suggestion
@UriParam(label = "security", secret = true, description = "Password for Basic authentication")
private String authPassword;
```
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * 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.odata;
+
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriParams;
+import org.apache.camel.support.jsse.SSLContextParameters;
+
+@UriParams
+public class ODataConfiguration {
+
+ @UriParam(label = "producer", description = "The OData operation to perform")
+ private ODataOperation operation = ODataOperation.READ_SET;
+
+ @UriParam(label = "producer", description = "The OData $filter query parameter")
+ private String filter;
+
+ @UriParam(label = "producer", description = "The OData $select query parameter")
+ private String select;
+
+ @UriParam(label = "producer", description = "The OData $expand query parameter")
+ private String expand;
+
+ @UriParam(label = "producer", description = "The OData $orderby query parameter")
+ private String orderBy;
+
+ @UriParam(label = "producer", description = "The OData $top query parameter")
+ private Integer top;
+
+ @UriParam(label = "producer", description = "The OData $skip query parameter")
+ private Integer skip;
+
+ @UriParam(label = "producer", description = "The OData $count query parameter")
+ private Boolean count;
+
+ @UriParam(label = "security", description = "Authentication method to use (e.g., Basic, Bearer)")
+ private String authMethod;
+
+ @UriParam(label = "security", description = "Username for Basic authentication")
+ private String authUsername;
+
+ @UriParam(label = "security", description = "Password for Basic authentication")
+ private String authPassword;
+
+ @UriParam(label = "security", description = "Bearer token for Bearer authentication")
+ private String authBearerToken;
+
+ @UriParam(label = "security", description = "To use a custom SSLContextParameters")
Review Comment:
🔴 **Security:** Same issue — bearer token must be marked as secret.
```suggestion
@UriParam(label = "security", secret = true, description = "Bearer token for Bearer authentication")
private String authBearerToken;
```
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataComponent.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.odata;
+
+import java.net.URI;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.camel.Endpoint;
+import org.apache.camel.spi.annotations.Component;
+import org.apache.camel.support.DefaultComponent;
+
+@Component("odata")
+public class ODataComponent extends DefaultComponent {
+
+ @Override
+ protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters)
+ throws Exception {
+
+ ODataEndpoint endpoint = new ODataEndpoint(uri, this);
+
+ endpoint.setHttpUri(new URI(remaining));
+
+ // Create a copy of parameters before setProperties consumes known properties
+ Map<String, Object> customParams = new LinkedHashMap<>(parameters);
Review Comment:
⚠️ **Dead code:** `customParams` is assigned but never read. Remove it.
```suggestion
```
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataHelper.java:
##########
@@ -0,0 +1,67 @@
+/*
+ * 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.odata;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.util.json.DeserializationException;
+import org.apache.camel.util.json.Jsoner;
+
+public final class ODataHelper {
+
+ private ODataHelper() {
+ }
+
+ public static String toJson(Object value) {
+ if (value == null) {
+ return null;
+ }
+
+ if (value instanceof String string) {
+ return string;
+ }
+
+ return Jsoner.serialize(value);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Map<String, Object> parseJsonObject(String value) throws DeserializationException {
+ if (value == null || value.isBlank()) {
+ return Map.of();
+ }
+
+ Object parsed = Jsoner.deserialize(value);
+ if (!(parsed instanceof Map)) {
+ throw new IllegalArgumentException("Expected an OData JSON object response");
+ }
+
+ return (Map<String, Object>) parsed;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static List<Map<String, Object>> extractValueList(Map<String, Object> response) {
+ Object value = response.get("value");
+
+ if (!(value instanceof List)) {
+ throw new IllegalArgumentException("OData response does not contain a value array");
+ }
+
+ return (List<Map<String, Object>>) value;
+ }
Review Comment:
⚠️ **Dead code:** `extractValueList()` is not called anywhere in the codebase. Either use it in `ODataProducer.process()` to parse `READ_SET` responses, or remove it.
##########
components/camel-odata/pom.xml:
##########
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>components</artifactId>
+ <version>4.23.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>camel-odata</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Camel :: OData</name>
+ <description>Camel OData Component</description>
+
+ <properties>
+ <firstVersion>4.23.0-SNAPSHOT</firstVersion>
Review Comment:
⚠️ **Convention:** `firstVersion` should be `4.23.0`, not `4.23.0-SNAPSHOT`. The generated `odata.json` under `src/generated/resources/odata.json` also has `4.23.0-SNAPSHOT` as `firstVersion` while the component descriptor under `META-INF` has the correct `4.23.0`. This inconsistency will persist in release artifacts.
```suggestion
<firstVersion>4.23.0</firstVersion>
```
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,359 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData (Open Data Protocol) services using standard OData CRUD operations.
+
+The component delegates HTTP transport to Camel HTTP while providing OData-specific operation, key, query option, authentication, and response handling.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+## [source,xml]
Review Comment:
🔴 **Documentation:** Every code block in this file uses `## [source,xml]` / `## [source,java]` instead of the correct AsciiDoc syntax `[source,xml]`. The `##` prefix is a Markdown heading marker and will render as literal text or garbled headings in the Antora-generated docs. This affects all ~17 code blocks in the file.
Additionally, lines 278 and 308 have unescaped quotes in Java examples:
```
.setHeader("CamelODataETag", constant("W/"etag-value""))
```
This is invalid Java. It should be:
```
.setHeader("CamelODataETag", constant("W/\"etag-value\""))
```
The entire AsciiDoc file needs a formatting pass — every code listing block delimiter and closing is broken.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4152149220
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * 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.odata;
+
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriParams;
+import org.apache.camel.support.jsse.SSLContextParameters;
+
+@UriParams
+public class ODataConfiguration {
+
+ @UriParam(label = "producer", description = "The OData operation to perform")
+ private ODataOperation operation = ODataOperation.READ_SET;
+
+ @UriParam(label = "producer", description = "The OData $filter query parameter")
+ private String filter;
+
+ @UriParam(label = "producer", description = "The OData $select query parameter")
+ private String select;
+
+ @UriParam(label = "producer", description = "The OData $expand query parameter")
+ private String expand;
+
+ @UriParam(label = "producer", description = "The OData $orderby query parameter")
+ private String orderBy;
+
+ @UriParam(label = "producer", description = "The OData $top query parameter")
+ private Integer top;
+
+ @UriParam(label = "producer", description = "The OData $skip query parameter")
+ private Integer skip;
+
+ @UriParam(label = "producer", description = "The OData $count query parameter")
+ private Boolean count;
+
+ @UriParam(label = "security", description = "Authentication method to use (e.g., Basic, Bearer)")
+ private String authMethod;
+
+ @UriParam(label = "security", description = "Username for Basic authentication")
+ private String authUsername;
+
+ @UriParam(label = "security", description = "Password for Basic authentication")
+ private String authPassword;
Review Comment:
🔴 **NOT addressed (re-raised):** `authPassword` is still not annotated with `secret = true`. The password will appear in plain text in endpoint URIs, logs, JMX, and management APIs. The generated `odata.json` still has `"secret": false` and `SECRET_PROPERTY_NAMES` is empty.
```suggestion
@UriParam(label = "security", secret = true, description = "Password for Basic authentication")
private String authPassword;
```
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataConfiguration.java:
##########
@@ -0,0 +1,168 @@
+/*
+ * 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.odata;
+
+import org.apache.camel.spi.UriParam;
+import org.apache.camel.spi.UriParams;
+import org.apache.camel.support.jsse.SSLContextParameters;
+
+@UriParams
+public class ODataConfiguration {
+
+ @UriParam(label = "producer", description = "The OData operation to perform")
+ private ODataOperation operation = ODataOperation.READ_SET;
+
+ @UriParam(label = "producer", description = "The OData $filter query parameter")
+ private String filter;
+
+ @UriParam(label = "producer", description = "The OData $select query parameter")
+ private String select;
+
+ @UriParam(label = "producer", description = "The OData $expand query parameter")
+ private String expand;
+
+ @UriParam(label = "producer", description = "The OData $orderby query parameter")
+ private String orderBy;
+
+ @UriParam(label = "producer", description = "The OData $top query parameter")
+ private Integer top;
+
+ @UriParam(label = "producer", description = "The OData $skip query parameter")
+ private Integer skip;
+
+ @UriParam(label = "producer", description = "The OData $count query parameter")
+ private Boolean count;
+
+ @UriParam(label = "security", description = "Authentication method to use (e.g., Basic, Bearer)")
+ private String authMethod;
+
+ @UriParam(label = "security", description = "Username for Basic authentication")
+ private String authUsername;
+
+ @UriParam(label = "security", description = "Password for Basic authentication")
+ private String authPassword;
+
+ @UriParam(label = "security", description = "Bearer token for Bearer authentication")
+ private String authBearerToken;
+
+ @UriParam(label = "security", description = "To use a custom SSLContextParameters")
Review Comment:
🔴 **NOT addressed (re-raised):** `authBearerToken` is still not annotated with `secret = true`. Same exposure risk as `authPassword`.
```suggestion
@UriParam(label = "security", secret = true, description = "Bearer token for Bearer authentication")
private String authBearerToken;
```
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataHelper.java:
##########
@@ -0,0 +1,67 @@
+/*
+ * 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.odata;
+
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.util.json.DeserializationException;
+import org.apache.camel.util.json.Jsoner;
+
+public final class ODataHelper {
+
+ private ODataHelper() {
+ }
+
+ public static String toJson(Object value) {
+ if (value == null) {
+ return null;
+ }
+
+ if (value instanceof String string) {
+ return string;
+ }
+
+ return Jsoner.serialize(value);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Map<String, Object> parseJsonObject(String value) throws DeserializationException {
+ if (value == null || value.isBlank()) {
+ return Map.of();
+ }
+
+ Object parsed = Jsoner.deserialize(value);
+ if (!(parsed instanceof Map)) {
+ throw new IllegalArgumentException("Expected an OData JSON object response");
+ }
+
+ return (Map<String, Object>) parsed;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static List<Map<String, Object>> extractValueList(Map<String, Object> response) {
+ Object value = response.get("value");
+
+ if (!(value instanceof List)) {
+ throw new IllegalArgumentException("OData response does not contain a value array");
+ }
+
+ return (List<Map<String, Object>>) value;
+ }
Review Comment:
⚠️ **NOT addressed (re-raised):** `extractValueList()` is not called anywhere in the codebase. Either use it in `ODataProducer.process()` to parse `READ_SET` responses, or remove it.
##########
components/camel-odata/pom.xml:
##########
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+ 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.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>components</artifactId>
+ <version>4.23.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>camel-odata</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Camel :: OData</name>
+ <description>Camel OData Component</description>
+
+ <properties>
+ <firstVersion>4.23.0-SNAPSHOT</firstVersion>
Review Comment:
⚠️ **NOT addressed (re-raised):** `firstVersion` should be `4.23.0`, not `4.23.0-SNAPSHOT`. This will persist the SNAPSHOT suffix in release artifacts.
```suggestion
<firstVersion>4.23.0</firstVersion>
```
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,359 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData (Open Data Protocol) services using standard OData CRUD operations.
+
+The component delegates HTTP transport to Camel HTTP while providing OData-specific operation, key, query option, authentication, and response handling.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+## [source,xml]
Review Comment:
🔴 **NOT addressed (re-raised):** Every code block in this file uses `## [source,xml]` / `## [source,java]` instead of the correct AsciiDoc syntax `[source,xml]`. The `##` prefix is a Markdown heading marker and will render as literal text or garbled headings in the Antora-generated docs. This affects all ~17 code blocks in the file.
Additionally, the ETag examples at lines 278 and 308 still have unescaped quotes in the Java code. The entire AsciiDoc file needs a formatting pass.
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataComponent.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.odata;
+
+import java.net.URI;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.camel.Endpoint;
+import org.apache.camel.spi.annotations.Component;
+import org.apache.camel.support.DefaultComponent;
+
+@Component("odata")
+public class ODataComponent extends DefaultComponent {
+
+ @Override
+ protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters)
+ throws Exception {
+
+ ODataEndpoint endpoint = new ODataEndpoint(uri, this);
+
+ endpoint.setHttpUri(new URI(remaining));
+
+ // Create a copy of parameters before setProperties consumes known properties
+ Map<String, Object> customParams = new LinkedHashMap<>(parameters);
Review Comment:
⚠️ **NOT addressed (re-raised):** `customParams` is assigned but never read — remove the dead copy.
```suggestion
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "davsclaus (via GitHub)" <gi...@apache.org>.
davsclaus commented on PR #27168:
URL: https://github.com/apache/camel/pull/27168#issuecomment-5947534551
LGTM
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4161608340
##########
components/camel-odata/src/test/java/org/apache/camel/component/odata/ODataProducerTest.java:
##########
@@ -0,0 +1,1143 @@
+/*
+ * 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.odata;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.http.base.HttpOperationFailedException;
+import org.apache.camel.support.jsse.KeyStoreParameters;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.support.jsse.TrustManagersParameters;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.containing;
+import static com.github.tomakehurst.wiremock.client.WireMock.delete;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.matching;
+import static com.github.tomakehurst.wiremock.client.WireMock.patch;
+import static com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ODataProducerTest extends CamelTestSupport {
+
+ @RegisterExtension
+ static WireMockExtension wireMock = WireMockExtension.newInstance()
+ .options(wireMockConfig()
+ .dynamicPort()
+ .dynamicHttpsPort()
+ .keystorePath(Objects.requireNonNull(ODataProducerTest.class.getResource("/localhost.p12")).getFile())
+ .keystoreType("PKCS12")
+ .keystorePassword("changeit")
+ .keyManagerPassword("changeit"))
+ .build();
+
+ private ODataEndpoint endpoint;
+ private Producer producer;
+
+ @BeforeEach
+ public void setUpProducer() throws Exception {
+ String uri = "odata:http://localhost:" + wireMock.getPort() + "/odata/Products";
+ endpoint = context.getEndpoint(uri, ODataEndpoint.class);
+ producer = endpoint.createProducer();
+ producer.start();
+ }
+
+ @AfterEach
+ public void tearDownProducer() throws Exception {
+ if (producer != null) {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessGetCollection() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[{\"ID\":101,\"Name\":\"Laptop\"},{\"ID\":102,\"Name\":\"Phone\"}]}")));
+
+ Exchange exchange = endpoint.createExchange();
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertTrue(body.containsKey("value"));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products")));
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithKeyHeaderDynamicUri() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":101,\"Name\":\"Laptop\"}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "101");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertEquals("Laptop", body.get("Name"));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithKeyInUri() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":101,\"Name\":\"Laptop\"}")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertEquals("Laptop", body.get("Name"));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessQueryOptionsHeaders() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*Laptop.*"))
+ .withQueryParam("$select", matching(".*ID.*Name.*"))
+ .withQueryParam("$expand", equalTo("Category"))
+ .withQueryParam("$orderby", matching(".*Name.*asc.*"))
+ .withQueryParam("$top", equalTo("5"))
+ .withQueryParam("$skip", equalTo("10"))
+ .withQueryParam("$count", equalTo("true"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"@odata.count\":1,\"value\":[{\"ID\":101,\"Name\":\"Laptop\"}]}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.FILTER, "Name eq 'Laptop'");
+ exchange.getMessage().setHeader(ODataConstants.SELECT, "ID,Name");
+ exchange.getMessage().setHeader(ODataConstants.EXPAND, "Category");
+ exchange.getMessage().setHeader(ODataConstants.ORDER_BY, "Name asc");
+ exchange.getMessage().setHeader(ODataConstants.TOP, 5);
+ exchange.getMessage().setHeader(ODataConstants.SKIP, 10);
+ exchange.getMessage().setHeader(ODataConstants.INCLUDE_COUNT, true);
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals(1L, exchange.getMessage().getHeader(ODataConstants.COUNT));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*Laptop.*"))
+ .withQueryParam("$select", matching(".*ID.*Name.*"))
+ .withQueryParam("$expand", equalTo("Category"))
+ .withQueryParam("$orderby", matching(".*Name.*asc.*"))
+ .withQueryParam("$top", equalTo("5"))
+ .withQueryParam("$skip", equalTo("10"))
+ .withQueryParam("$count", equalTo("true")));
+ }
+
+ @Test
+ void testProcessCreateEntity() throws Exception {
+ wireMock.stubFor(post(urlPathEqualTo("/odata/Products"))
+ .withRequestBody(equalToJson("{\"Name\":\"Tablet\",\"Price\":299.99}"))
+ .willReturn(aResponse()
+ .withStatus(201)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":103,\"Name\":\"Tablet\",\"Price\":299.99}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.CREATE);
+
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("Name", "Tablet");
+ payload.put("Price", 299.99);
+ exchange.getMessage().setBody(payload);
+
+ producer.process(exchange);
+
+ assertEquals(201, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ Map<?, ?> body = exchange.getMessage().getBody(Map.class);
+ assertNotNull(body);
+ assertEquals(new BigDecimal(103), body.get("ID"));
+ }
+
+ @Test
+ void testProcessIgnoresInboundHttpRoutingHeaders() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange = endpoint.createExchange();
+
+ // Simulate HTTP/REST headers inherited from an upstream consumer.
+ exchange.getMessage().setHeader(Exchange.HTTP_URI, "http://malicious.example/other");
+ exchange.getMessage().setHeader(Exchange.HTTP_PATH, "/wrong/path");
+ exchange.getMessage().setHeader(Exchange.HTTP_QUERY, "unexpected=true");
+ exchange.getMessage().setHeader(Exchange.HTTP_RAW_QUERY, "rawUnexpected=true");
+ exchange.getMessage().setHeader("CamelRestHttpUri", "http://malicious.example/rest");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products")));
+ }
+
+ @Test
+ void testProcessUpdateEntity() throws Exception {
+ ODataEndpoint keyEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer keyProducer = keyEndpoint.createProducer();
+ keyProducer.start();
+
+ try {
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withRequestBody(equalToJson("{\"Name\":\"Updated Laptop\"}"))
+ .willReturn(aResponse().withStatus(204)));
+
+ Exchange exchange = keyEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.UPDATE);
+
+ Map<String, Object> payload = new HashMap<>();
+ payload.put("Name", "Updated Laptop");
+ exchange.getMessage().setBody(payload);
+
+ keyProducer.process(exchange);
+
+ assertEquals(204, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ } finally {
+ keyProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessDeleteEntity() throws Exception {
+ ODataEndpoint keyEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer keyProducer = keyEndpoint.createProducer();
+ keyProducer.start();
+
+ try {
+ wireMock.stubFor(delete(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse().withStatus(204)));
+
+ Exchange exchange = keyEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.DELETE);
+
+ keyProducer.process(exchange);
+
+ assertEquals(204, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNull(exchange.getMessage().getBody());
+ } finally {
+ keyProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithETagHeader() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("\"etag-v1\""))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"ID\":101,\"Name\":\"Laptop\",\"@odata.etag\":\"etag-v2\"}")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.ETAG, "\"etag-v1\"");
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("etag-v2", exchange.getMessage().getHeader(ODataConstants.ETAG));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessGetSingleEntityWithHttpETag() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(102)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(102)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withHeader("ETag", "W/\"etag-v3\"")
+ .withBody("{\"ID\":102,\"Name\":\"Monitor\"}")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("W/\"etag-v3--gzip\"", exchange.getMessage().getHeader(ODataConstants.ETAG));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessUpdateEntityWithETag() throws Exception {
+ ODataEndpoint updateEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer updateProducer = updateEndpoint.createProducer();
+ updateProducer.start();
+
+ try {
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"etag-v3\""))
+ .withHeader("Content-Type", containing("application/json"))
+ .withRequestBody(equalToJson("{\"Name\":\"Updated Laptop\"}"))
+ .willReturn(aResponse()
+ .withStatus(204)
+ .withHeader("ETag", "W/\"etag-v4\"")));
+
+ Exchange exchange = updateEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.UPDATE);
+ exchange.getMessage().setHeader(ODataConstants.ETAG, "W/\"etag-v3\"");
+ exchange.getMessage().setBody("{\"Name\":\"Updated Laptop\"}");
+
+ updateProducer.process(exchange);
+
+ assertEquals(204, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("W/\"etag-v4\"", exchange.getMessage().getHeader(ODataConstants.ETAG));
+
+ wireMock.verify(patchRequestedFor(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"etag-v3\"")));
+ } finally {
+ updateProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessUpdateEntityWithETagPreconditionFailed() throws Exception {
+ ODataEndpoint updateEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer updateProducer = updateEndpoint.createProducer();
+ updateProducer.start();
+
+ try {
+ wireMock.stubFor(patch(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"stale-etag\""))
+ .willReturn(aResponse()
+ .withStatus(412)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "error": {
+ "code": "412",
+ "message": "The ETag does not match."
+ }
+ }
+ """)));
+
+ Exchange exchange = updateEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.UPDATE);
+ exchange.getMessage().setHeader(ODataConstants.ETAG, "W/\"stale-etag\"");
+ exchange.getMessage().setBody("{\"Name\":\"Updated Laptop\"}");
+
+ HttpOperationFailedException exception = assertThrows(
+ HttpOperationFailedException.class,
+ () -> updateProducer.process(exchange));
+
+ assertEquals(412, exception.getStatusCode());
+ wireMock.verify(patchRequestedFor(urlPathEqualTo("/odata/Products(101)"))
+ .withHeader("If-Match", equalTo("W/\"stale-etag\"")));
+ } finally {
+ updateProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadSetWithEncodedFilter() throws Exception {
+ ODataEndpoint readEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer readProducer = readEndpoint.createProducer();
+ readProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo("Name eq 'Laptop Pro'"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "value": [
+ {"ID": 101, "Name": "Laptop Pro"}
+ ]
+ }
+ """)));
+
+ Exchange exchange = readEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_SET);
+ exchange.getMessage().setHeader(ODataConstants.FILTER, "Name eq 'Laptop Pro'");
+
+ readProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo("Name eq 'Laptop Pro'")));
+ } finally {
+ readProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadSetWithComplexEncodedFilter() throws Exception {
+ ODataEndpoint readEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer readProducer = readEndpoint.createProducer();
+ readProducer.start();
+
+ String filter = "Name eq 'O''Reilly' and Category eq 'Office/Supplies' "
+ + "and Price gt 10.5 and (Status eq 'Active' or Code eq 'A%20B') "
+ + "and startswith(Name,'Pro')";
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo(filter))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "value": [
+ {
+ "ID": 101,
+ "Name": "O'Reilly Pro",
+ "Category": "Office/Supplies"
+ }
+ ]
+ }
+ """)));
+
+ Exchange exchange = readEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_SET);
+ exchange.getMessage().setHeader(ODataConstants.FILTER, filter);
+
+ readProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", equalTo(filter)));
+ } finally {
+ readProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadEntryWithTrailingSlashAndKey() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products/",
+ ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "ID": 101,
+ "Name": "Laptop"
+ }
+ """)));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "101");
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products(101)")));
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessNextLinkHeader() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[],\"@odata.nextLink\":\"http://localhost/odata/Products?$skip=10\"}")));
+
+ Exchange exchange = endpoint.createExchange();
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertEquals("http://localhost/odata/Products?$skip=10", exchange.getMessage().getHeader(ODataConstants.NEXT_LINK));
+ }
+
+ @Test
+ void testProcessEndpointConfigurationDefaults() throws Exception {
+ ODataEndpoint configuredEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?top=5&skip=10&filter=Name eq 'Phone'",
+ ODataEndpoint.class);
+ Producer configuredProducer = configuredEndpoint.createProducer();
+ configuredProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$top", equalTo("5"))
+ .withQueryParam("$skip", equalTo("10"))
+ .withQueryParam("$filter", matching(".*Name.*Phone.*"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[{\"ID\":102,\"Name\":\"Phone\"}]}")));
+
+ Exchange exchange = configuredEndpoint.createExchange();
+ configuredProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ } finally {
+ configuredProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessHeaderOverridesEndpointConfiguration() throws Exception {
+ ODataEndpoint configuredEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?top=5",
+ ODataEndpoint.class);
+ Producer configuredProducer = configuredEndpoint.createProducer();
+ configuredProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$top", equalTo("20"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange = configuredEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.TOP, 20);
+
+ configuredProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ } finally {
+ configuredProducer.stop();
+ }
+ }
+
+ @Test
+ void testProcessHttpErrorPropagatesException() {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse().withStatus(500)));
+
+ Exchange exchange = endpoint.createExchange();
+
+ assertThrows(HttpOperationFailedException.class, () -> producer.process(exchange));
+ }
+
+ @Test
+ void testProcessEmptyResponseBodyReturnsNullBody() throws Exception {
+ ODataEndpoint singleEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products(101)", ODataEndpoint.class);
+ Producer singleProducer = singleEndpoint.createProducer();
+ singleProducer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products(101)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("")));
+
+ Exchange exchange = singleEndpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+
+ singleProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNull(exchange.getMessage().getBody());
+ } finally {
+ singleProducer.stop();
+ }
+ }
+
+ @Test
+ void testProducerSingletonAndLifecycle() {
+ assertTrue(producer.isSingleton());
+ }
+
+ @Test
+ void testBasicAuthentication() throws Exception {
+ String username = "adminUser";
+ String password = "adminPassword123";
+ String expectedHeader
+ = "Basic " + Base64.getEncoder().encodeToString((username + ":" + password).getBytes(StandardCharsets.UTF_8));
+
+ ODataEndpoint authEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authMethod=Basic&authUsername=" + username
+ + "&authPassword=" + password,
+ ODataEndpoint.class);
+ Producer authProducer = authEndpoint.createProducer();
+ authProducer.start();
+
+ try {
+ // Priority 1: Match and return 200 OK when correct credentials are present
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(1)
+ .withHeader("Authorization", equalTo(expectedHeader))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ // Priority 10: Fallback HTTP Basic 401 challenge with WWW-Authenticate header
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(10)
+ .willReturn(aResponse()
+ .withStatus(401)
+ .withHeader("WWW-Authenticate", "Basic realm=\"OData Test Realm\"")));
+
+ Exchange exchange = authEndpoint.createExchange();
+ authProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(expectedHeader)));
+ } finally {
+ authProducer.stop();
+ }
+ }
+
+ @Test
+ void testBearerAuthenticationAndExchangeHeaderPrecedence() throws Exception {
+ String configuredToken = "configuredBearerToken123";
+ String explicitHeaderToken = "Bearer explicitHeaderToken456";
+
+ ODataEndpoint authEndpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authBearerToken=" + configuredToken,
+ ODataEndpoint.class);
+ Producer authProducer = authEndpoint.createProducer();
+ authProducer.start();
+
+ try {
+ // Case 1: Configured bearer token attached automatically
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + configuredToken))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange1 = authEndpoint.createExchange();
+ authProducer.process(exchange1);
+ assertEquals(200, exchange1.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + configuredToken)));
+
+ // Case 2: Explicit Exchange Authorization header takes precedence over configured token
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(explicitHeaderToken))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange2 = authEndpoint.createExchange();
+ exchange2.getMessage().setHeader("Authorization", explicitHeaderToken);
+ authProducer.process(exchange2);
+ assertEquals(200, exchange2.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(explicitHeaderToken)));
+ } finally {
+ authProducer.stop();
+ }
+ }
+
+ @Test
+ void testBearerEndpointConcurrentIsolation() throws Exception {
+ String token1 = "tokenUnit1";
+ String token2 = "tokenUnit2";
+
+ ODataEndpoint endpoint1 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?authBearerToken=" + token1,
+ ODataEndpoint.class);
+ ODataEndpoint endpoint2 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products?authBearerToken=" + token2,
+ ODataEndpoint.class);
+
+ Producer producer1 = endpoint1.createProducer();
+ Producer producer2 = endpoint2.createProducer();
+
+ producer1.start();
+ producer2.start();
+
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ int threadCount = 10;
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ ConcurrentLinkedQueue<Throwable> failures = new ConcurrentLinkedQueue<>();
+
+ try {
+ for (int i = 0; i < threadCount; i++) {
+ final int index = i;
+ executor.submit(() -> {
+ Exchange exchange = null;
+ try {
+ Producer targetProducer = (index % 2 == 0) ? producer1 : producer2;
+ ODataEndpoint targetEndpoint = (index % 2 == 0) ? endpoint1 : endpoint2;
+ exchange = targetEndpoint.createExchange();
+ targetProducer.process(exchange);
+ if (exchange.getException() != null) {
+ failures.add(exchange.getException());
+ }
+ } catch (Throwable t) {
+ failures.add(t);
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ assertTrue(failures.isEmpty(), "Worker threads encountered exceptions: " + failures);
+
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + token1)));
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Bearer " + token2)));
+ } finally {
+ executor.shutdownNow();
+ producer1.stop();
+ producer2.stop();
+ }
+ }
+
+ @Test
+ void testBasicAuthConcurrentIsolation() throws Exception {
+ String user1 = "user1";
+ String pass1 = "pass1";
+ String expectedHeader1
+ = "Basic " + Base64.getEncoder().encodeToString((user1 + ":" + pass1).getBytes(StandardCharsets.UTF_8));
+
+ String user2 = "user2";
+ String pass2 = "pass2";
+ String expectedHeader2
+ = "Basic " + Base64.getEncoder().encodeToString((user2 + ":" + pass2).getBytes(StandardCharsets.UTF_8));
+
+ ODataEndpoint endpoint1 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authMethod=Basic&authUsername=" + user1
+ + "&authPassword=" + pass1,
+ ODataEndpoint.class);
+ ODataEndpoint endpoint2 = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort()
+ + "/odata/Products?authMethod=Basic&authUsername=" + user2
+ + "&authPassword=" + pass2,
+ ODataEndpoint.class);
+
+ Producer producer1 = endpoint1.createProducer();
+ Producer producer2 = endpoint2.createProducer();
+
+ producer1.start();
+ producer2.start();
+
+ int threadCount = 10;
+ ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+
+ try {
+ // Priority 1: Match and return 200 OK when user1 credentials are present
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(1)
+ .withHeader("Authorization", equalTo(expectedHeader1))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ // Priority 1: Match and return 200 OK when user2 credentials are present
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(1)
+ .withHeader("Authorization", equalTo(expectedHeader2))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ // Priority 10: Fallback HTTP Basic 401 challenge with WWW-Authenticate header
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .atPriority(10)
+ .willReturn(aResponse()
+ .withStatus(401)
+ .withHeader("WWW-Authenticate", "Basic realm=\"OData Test Realm\"")));
+
+ CountDownLatch latch = new CountDownLatch(threadCount);
+ ConcurrentLinkedQueue<Throwable> failures = new ConcurrentLinkedQueue<>();
+
+ for (int i = 0; i < threadCount; i++) {
+ final int index = i;
+ executor.submit(() -> {
+ Exchange exchange = null;
+ try {
+ Producer targetProducer = (index % 2 == 0) ? producer1 : producer2;
+ ODataEndpoint targetEndpoint = (index % 2 == 0) ? endpoint1 : endpoint2;
+ exchange = targetEndpoint.createExchange();
+ targetProducer.process(exchange);
+ if (exchange.getException() != null) {
+ failures.add(exchange.getException());
+ }
+ } catch (Throwable t) {
+ failures.add(t);
+ } finally {
+ latch.countDown();
+ }
+ });
+ }
+
+ assertTrue(latch.await(10, TimeUnit.SECONDS));
+ assertTrue(failures.isEmpty(), "Worker threads encountered exceptions: " + failures);
+
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(expectedHeader1)));
+ wireMock.verify(threadCount / 2, getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo(expectedHeader2)));
+ } finally {
+ executor.shutdownNow();
+ producer1.stop();
+ producer2.stop();
+ }
+ }
+
+ @Test
+ void testQueryEncodingWithAmpersand() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*John.*Sons.*"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.FILTER, "Name eq 'John & Sons'");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withQueryParam("$filter", matching(".*Name.*John.*Sons.*")));
+ }
+
+ @Test
+ void testProcessReadEntryWithStringKey() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer producer = endpoint.createProducer();
+ producer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products('ABC-123')"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {"ID":"ABC-123","Name":"Laptop"}
+ """)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(ODataConstants.KEY, "'ABC-123'");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products('ABC-123')")));
+ } finally {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessReadEntryWithCompositeKey() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+ Producer producer = endpoint.createProducer();
+ producer.start();
+
+ try {
+ wireMock.stubFor(get(urlPathEqualTo(
+ "/odata/Products(ProductID=101,CategoryID=5)"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {"ProductID":101,"CategoryID":5,"Name":"Laptop"}
+ """)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_ENTRY);
+ exchange.getMessage().setHeader(
+ ODataConstants.KEY, "ProductID=101,CategoryID=5");
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo(
+ "/odata/Products(ProductID=101,CategoryID=5)")));
+ } finally {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessWithConfiguredBasicAuthentication() throws Exception {
+ ODataEndpoint endpoint = context.getEndpoint(
+ "odata:http://localhost:" + wireMock.getPort() + "/odata/Products",
+ ODataEndpoint.class);
+
+ endpoint.getConfiguration().setAuthMethod("Basic");
+ endpoint.getConfiguration().setAuthUsername("odata-user");
+ endpoint.getConfiguration().setAuthPassword("odata-password");
+
+ Producer producer = endpoint.createProducer();
+ producer.start();
+
+ try {
+ String credentials = Base64.getEncoder()
+ .encodeToString("odata-user:odata-password".getBytes(StandardCharsets.UTF_8));
+
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Basic " + credentials))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("""
+ {
+ "value": []
+ }
+ """)));
+
+ Exchange exchange = endpoint.createExchange();
+ exchange.getMessage().setHeader(ODataConstants.OPERATION, ODataOperation.READ_SET);
+
+ producer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+
+ wireMock.verify(getRequestedFor(urlPathEqualTo("/odata/Products"))
+ .withHeader("Authorization", equalTo("Basic " + credentials)));
+ } finally {
+ producer.stop();
+ }
+ }
+
+ @Test
+ void testProcessHttpsWithSslContextParameters() throws Exception {
+ wireMock.stubFor(get(urlPathEqualTo("/odata/Products"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"value\":[]}")));
+
+ KeyStoreParameters trustStore = new KeyStoreParameters();
+ trustStore.setResource("localhost.p12");
+ trustStore.setPassword("changeit");
+ trustStore.setType("PKCS12");
+
+ TrustManagersParameters trustManagers = new TrustManagersParameters();
+ trustManagers.setKeyStore(trustStore);
+
+ SSLContextParameters sslContextParameters = new SSLContextParameters();
+ sslContextParameters.setTrustManagers(trustManagers);
+
+ String baseUri = "odata:https://localhost:" + wireMock.getHttpsPort() + "/odata/Products";
+
+ // Use different OData endpoint URIs while keeping the underlying HTTP URI identical.
+ ODataEndpoint httpsEndpoint = context.getEndpoint(
+ baseUri + "?useGlobalSslContextParameters=true", ODataEndpoint.class);
+ httpsEndpoint.setSslContextParameters(sslContextParameters);
+
+ ODataEndpoint endpointWithoutSsl = context.getEndpoint(
+ baseUri + "?useGlobalSslContextParameters=false", ODataEndpoint.class);
+
+ Producer httpsProducer = httpsEndpoint.createProducer();
+ Producer endpointWithoutSslProducer = endpointWithoutSsl.createProducer();
+
+ try {
+ httpsProducer.start();
+
+ Exchange exchange = httpsEndpoint.createExchange();
+ httpsProducer.process(exchange);
+
+ assertEquals(200, exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE));
+ assertNotNull(exchange.getMessage().getBody());
+
+ endpointWithoutSslProducer.start();
+
+ Exchange exchangeWithoutSsl = endpointWithoutSsl.createExchange();
+
+ Throwable thrown = assertThrows(Throwable.class, () -> endpointWithoutSslProducer.process(exchangeWithoutSsl));
+
+ // Check for SSLException / SSLHandshakeException in the cause chain
+ boolean hasSslCause = false;
+ Throwable current = thrown;
+ while (current != null) {
+ if (current instanceof javax.net.ssl.SSLException) { // Use jakarta.net.ssl.SSLException if on Jakarta/Camel 4+
+ hasSslCause = true;
Review Comment:
addressed
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154435575
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,406 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData V4 services using standard OData CRUD operations.
+
+The component provides OData-specific request and response handling while using Camel HTTP as the underlying HTTP transport.
+
+The OData component is the successor to the deprecated `camel-olingo4` component.
+
+The component supports OData V4 services using JSON representations.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+[source,xml]
+
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-odata</artifactId>
+ <version>x.x.x</version>
+ <!-- use the same version as your Camel core version -->
+</dependency>
+----
+
+== URI Format
+
+=== odata:httpUri
+
+Where `httpUri` is the base URL of the OData entity set or resource.
+
+For example:
+
+[source,text]
+
+odata:http://localhost:8080/odata/Products
+odata:https://services.odata.org/V4/Northwind/Northwind.svc/Categories
+----------------------------------------------------------------------
+
+The URI can also contain endpoint options as query parameters.
+
+The OData query options can alternatively be configured on the endpoint or supplied dynamically using message headers.
+
+The component uses Camel HTTP for the underlying HTTP transport.
+
+// component options: START
+include::partial$component-configure-options.adoc[]
+include::partial$component-endpoint-options.adoc[]
+include::partial$component-endpoint-headers.adoc[]
+// component options: END
+
+# [NOTE]
Review Comment:
Addressed in the latest commit ee3cc3b4f7a4.
The # [NOTE] blocks were replaced with valid AsciiDoc [NOTE] admonition blocks using ==== delimiters.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "gnodet-bot (via GitHub)" <gi...@apache.org>.
gnodet-bot commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4154434061
##########
components/camel-odata/src/main/docs/odata-component.adoc:
##########
@@ -0,0 +1,408 @@
+= OData Component
+:doctitle: OData
+:shortname: odata
+:artifactid: camel-odata
+:description: Camel OData Component
+:since: 4.23
+:supportlevel: Preview
+:tabs-sync-option:
+:component-header: Only producer is supported
+
+*Since Camel {since}*
+
+*{component-header}*
+
+The OData component allows you to interact with OData V4 services using standard OData CRUD operations.
+
+The component provides OData-specific request and response handling while using Camel HTTP as the underlying HTTP transport.
+
+The OData component is the successor to the deprecated `camel-olingo4` component.
+
+The component supports OData V4 services using JSON representations.
+
+Maven users will need to add the following dependency to their `pom.xml` for this component:
+
+[source,xml]
+
+<dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-odata</artifactId>
+ <version>x.x.x</version>
+ <!-- use the same version as your Camel core version -->
+</dependency>
+----
Review Comment:
🔴 **AsciiDoc — every listing block is missing its opening `----` delimiter.**
The current pattern throughout the file is:
```
[source,xml]
<dependency>
...
</dependency>
----
```
But valid AsciiDoc listing blocks **require both delimiters**:
```
[source,xml]
----
<dependency>
...
</dependency>
----
```
This is exactly what the Antora CI reports as `unterminated listing block` at line 29 (see [apupier's comment](https://github.com/apache/camel/pull/27168#issuecomment-3029003700)). The missing opening `----` causes AsciiDoc to treat the closing `----` as the *opening*, and then everything after it becomes code until the next delimiter — cascading through the rest of the document.
This affects **all ~18 code blocks** in the file (lines 25, 43, 106, 114, 195, 241, 253, 261, 280, 300, 316, 336, 348, 358, 364, 376, 392). Each `[source,...]` must be followed immediately by `----` on the next line (no blank line between them).
Additionally, line 312 has a single `-` instead of `----` as its closing delimiter.
The fix is mechanical: insert `----` after each `[source,...]` line (removing the blank line), and fix the single `-` at line 312 to `----`.
This is the same class of issue that was raised in prior reviews (the `## [source,...]` Markdown prefix) — the prefix was removed, but the opening delimiter was not added back.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] CAMEL-22813: Add OData component [camel]
Posted by "ramu11 (via GitHub)" <gi...@apache.org>.
ramu11 commented on code in PR #27168:
URL: https://github.com/apache/camel/pull/27168#discussion_r4156204796
##########
components/camel-odata/src/main/java/org/apache/camel/component/odata/ODataProducer.java:
##########
@@ -0,0 +1,296 @@
+/*
+ * 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.odata;
+
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
+import org.apache.camel.Producer;
+import org.apache.camel.component.http.HttpConstants;
+import org.apache.camel.support.DefaultProducer;
+import org.apache.camel.support.MessageHelper;
+import org.apache.camel.support.jsse.SSLContextParameters;
+import org.apache.camel.support.service.ServiceHelper;
+import org.apache.camel.util.URISupport;
+
+public class ODataProducer extends DefaultProducer {
+
+ private final ODataEndpoint endpoint;
+ private Producer httpProducer;
+
+ public ODataProducer(ODataEndpoint endpoint) {
+ super(endpoint);
+ this.endpoint = endpoint;
+ }
+
+ @Override
+ protected void doStart() throws Exception {
+ super.doStart();
+
+ URI uri = endpoint.getHttpUri();
+ String httpUri = uri.toString();
+
+ if (httpUri.contains("?")) {
+ httpUri += "&throwExceptionOnFailure=true";
+ } else {
+ httpUri += "?throwExceptionOnFailure=true";
+ }
+
+ ODataConfiguration config = endpoint.getConfiguration();
+ Map<String, Object> httpParameters = new LinkedHashMap<>();
+
+ if (config != null) {
+ SSLContextParameters sslParams = endpoint.getSslContextParameters();
+ if (sslParams == null && endpoint.isUseGlobalSslContextParameters()) {
+ sslParams = endpoint.getCamelContext().getSSLContextParameters();
+ }
+ if (sslParams != null) {
+ httpParameters.put("sslContextParameters", sslParams);
+ }
+ }
+
+ Endpoint httpEndpoint = endpoint.getCamelContext()
Review Comment:
Addressed in commit `c1a11055ded4`.
The OData producer now creates a private HTTP endpoint using `Component.createEndpoint(...)` instead of `CamelContext.getEndpoint(uri, parameters)`, so HTTP endpoint caching cannot cause SSL parameters to be shared between OData endpoints.
The private HTTP endpoint is also explicitly started and stopped with the OData producer lifecycle.
Added a regression test covering two OData endpoints with the same underlying HTTPS URL, where one endpoint uses SSL trust-store parameters and the other does not. The configured endpoint succeeds, while the endpoint without SSL configuration fails, verifying that the TLS configuration is isolated.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@camel.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org