This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-26025-api-components-bugs in repository https://gitbox.apache.org/repos/asf/camel.git
commit 03fc5a718b4e980a31d9c44ccf07de748ae3df33 Author: Claus Ibsen <[email protected]> AuthorDate: Fri Oct 23 16:49:16 2026 +0200 CAMEL-26025: camel-support - API components convert the inBody body, and an unknown operation of a header-selected producer fails - HeaderSelectorProducer fails for a value no method handles: generated InvokeOnHeaderStrategy classes return the new InvokeOnHeaderStrategy.NO_METHOD for an unknown key (regenerated for caffeine, consul, dynamic-router, ehcache, infinispan, servicenow, spring-redis and web3j) - AbstractApiProducer converts the inBody body to the type of the method parameter - ApiMethodPropertiesHelper ignores a header named as the prefix of the component - AbstractApiConfiguration.splitResult defaults to true, as documented - BodyFacts.kindOf records a Map that is also Iterable as data Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- ...CaffeineCacheProducerInvokeOnHeaderFactory.java | 2 +- ...eineLoadCacheProducerInvokeOnHeaderFactory.java | 2 +- .../caffeine/cache/CaffeineCacheProducerTest.java | 18 ++ .../ConsulAgentProducerInvokeOnHeaderFactory.java | 2 +- ...ConsulCatalogProducerInvokeOnHeaderFactory.java | 2 +- ...ulCoordinatesProducerInvokeOnHeaderFactory.java | 2 +- .../ConsulEventProducerInvokeOnHeaderFactory.java | 2 +- .../ConsulHealthProducerInvokeOnHeaderFactory.java | 2 +- ...onsulKeyValueProducerInvokeOnHeaderFactory.java | 2 +- ...PreparedQueryProducerInvokeOnHeaderFactory.java | 2 +- ...ConsulSessionProducerInvokeOnHeaderFactory.java | 2 +- .../ConsulStatusProducerInvokeOnHeaderFactory.java | 2 +- ...RouterControlProducerInvokeOnHeaderFactory.java | 2 +- .../EhcacheProducerInvokeOnHeaderFactory.java | 2 +- .../GoogleSheetsConsumerSplitResultTest.java | 86 +++++++++ .../InfinispanProducerInvokeOnHeaderFactory.java | 2 +- ...ispanEmbeddedProducerInvokeOnHeaderFactory.java | 2 +- ...inispanRemoteProducerInvokeOnHeaderFactory.java | 2 +- ...ujiServiceNowProducerInvokeOnHeaderFactory.java | 2 +- ...nkiServiceNowProducerInvokeOnHeaderFactory.java | 2 +- .../redis/RedisProducerInvokeOnHeaderFactory.java | 2 +- .../web3j/Web3jProducerInvokeOnHeaderFactory.java | 2 +- .../apache/camel/spi/InvokeOnHeaderStrategy.java | 16 +- .../org/apache/camel/support/BodyFactsTest.java | 19 ++ .../camel/support/HeaderSelectorProducerTest.java | 208 +++++++++++++++++++++ .../component/AbstractApiConfigurationTest.java} | 20 +- .../support/component/AbstractApiProducerTest.java | 200 ++++++++++++++++++++ .../component/ApiMethodPropertiesHelperTest.java | 16 ++ ...support.HeaderSelectorProducerTest$BaseProducer | 18 ++ ...upport.HeaderSelectorProducerTest$ChildProducer | 18 ++ ...pport.HeaderSelectorProducerTest$LegacyProducer | 18 ++ .../java/org/apache/camel/support/BodyFacts.java | 3 + .../camel/support/HeaderSelectorProducer.java | 14 +- .../component/AbstractApiConfiguration.java | 2 +- .../support/component/AbstractApiProducer.java | 26 ++- .../component/ApiMethodPropertiesHelper.java | 4 + .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 32 ++++ .../main/resources/velocity/invoke-on-header.vm | 2 +- 38 files changed, 715 insertions(+), 45 deletions(-) diff --git a/components/camel-caffeine/src/generated/java/org/apache/camel/component/caffeine/cache/CaffeineCacheProducerInvokeOnHeaderFactory.java b/components/camel-caffeine/src/generated/java/org/apache/camel/component/caffeine/cache/CaffeineCacheProducerInvokeOnHeaderFactory.java index c4c579ece82a..afb539ddc00a 100644 --- a/components/camel-caffeine/src/generated/java/org/apache/camel/component/caffeine/cache/CaffeineCacheProducerInvokeOnHeaderFactory.java +++ b/components/camel-caffeine/src/generated/java/org/apache/camel/component/caffeine/cache/CaffeineCacheProducerInvokeOnHeaderFactory.java @@ -35,7 +35,7 @@ public class CaffeineCacheProducerInvokeOnHeaderFactory implements InvokeOnHeade case "PUT": target.onPut(exchange.getMessage()); return null; case "put_all": case "PUT_ALL": target.onPutAll(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-caffeine/src/generated/java/org/apache/camel/component/caffeine/load/CaffeineLoadCacheProducerInvokeOnHeaderFactory.java b/components/camel-caffeine/src/generated/java/org/apache/camel/component/caffeine/load/CaffeineLoadCacheProducerInvokeOnHeaderFactory.java index 2765e3f7bd52..f99c4bd114d0 100644 --- a/components/camel-caffeine/src/generated/java/org/apache/camel/component/caffeine/load/CaffeineLoadCacheProducerInvokeOnHeaderFactory.java +++ b/components/camel-caffeine/src/generated/java/org/apache/camel/component/caffeine/load/CaffeineLoadCacheProducerInvokeOnHeaderFactory.java @@ -35,7 +35,7 @@ public class CaffeineLoadCacheProducerInvokeOnHeaderFactory implements InvokeOnH case "PUT": target.onPut(exchange.getMessage()); return null; case "put_all": case "PUT_ALL": target.onPutAll(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-caffeine/src/test/java/org/apache/camel/component/caffeine/cache/CaffeineCacheProducerTest.java b/components/camel-caffeine/src/test/java/org/apache/camel/component/caffeine/cache/CaffeineCacheProducerTest.java index f225f45bbc32..58a3ecef54ff 100644 --- a/components/camel-caffeine/src/test/java/org/apache/camel/component/caffeine/cache/CaffeineCacheProducerTest.java +++ b/components/camel-caffeine/src/test/java/org/apache/camel/component/caffeine/cache/CaffeineCacheProducerTest.java @@ -29,6 +29,7 @@ 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.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -258,6 +259,23 @@ public class CaffeineCacheProducerTest extends CaffeineCacheTestSupport { }); } + // **************************** + // Unknown action + // **************************** + + @Test + void testUnknownActionFails() throws Exception { + MockEndpoint mock = getMockEndpoint("mock:result"); + mock.expectedMessageCount(0); + + Exchange exchange = fluentTemplate().withHeader(CaffeineConstants.ACTION, "NOSUCH").withBody("Hello") + .to("direct://start").send(); + + IllegalArgumentException e = assertInstanceOf(IllegalArgumentException.class, exchange.getException()); + assertEquals("Unsupported value NOSUCH of header " + CaffeineConstants.ACTION, e.getMessage()); + MockEndpoint.assertIsSatisfied(context); + } + // **************************** // Route // **************************** diff --git a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulAgentProducerInvokeOnHeaderFactory.java b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulAgentProducerInvokeOnHeaderFactory.java index 64e4b0852265..952cf410e234 100644 --- a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulAgentProducerInvokeOnHeaderFactory.java +++ b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulAgentProducerInvokeOnHeaderFactory.java @@ -31,7 +31,7 @@ public class ConsulAgentProducerInvokeOnHeaderFactory implements InvokeOnHeaderS case "REGISTER": target.invokeRegister(exchange.getMessage()); return null; case "services": case "SERVICES": return target.invokeServices(exchange.getMessage()); - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulCatalogProducerInvokeOnHeaderFactory.java b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulCatalogProducerInvokeOnHeaderFactory.java index 049a29161c16..a4bba6f00ef3 100644 --- a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulCatalogProducerInvokeOnHeaderFactory.java +++ b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulCatalogProducerInvokeOnHeaderFactory.java @@ -33,7 +33,7 @@ public class ConsulCatalogProducerInvokeOnHeaderFactory implements InvokeOnHeade case "LIST_SERVICES": target.listServices(exchange.getMessage()); return null; case "register": case "REGISTER": target.register(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulCoordinatesProducerInvokeOnHeaderFactory.java b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulCoordinatesProducerInvokeOnHeaderFactory.java index 57648b71366b..5fff06739145 100644 --- a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulCoordinatesProducerInvokeOnHeaderFactory.java +++ b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulCoordinatesProducerInvokeOnHeaderFactory.java @@ -23,7 +23,7 @@ public class ConsulCoordinatesProducerInvokeOnHeaderFactory implements InvokeOnH case "DATACENTERS": target.datacenters(exchange.getMessage()); return null; case "nodes": case "NODES": target.nodes(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulEventProducerInvokeOnHeaderFactory.java b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulEventProducerInvokeOnHeaderFactory.java index deb2c9e450c5..6db9ea36b5df 100644 --- a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulEventProducerInvokeOnHeaderFactory.java +++ b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulEventProducerInvokeOnHeaderFactory.java @@ -23,7 +23,7 @@ public class ConsulEventProducerInvokeOnHeaderFactory implements InvokeOnHeaderS case "FIRE": target.fire(exchange.getMessage()); return null; case "list": case "LIST": target.list(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulHealthProducerInvokeOnHeaderFactory.java b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulHealthProducerInvokeOnHeaderFactory.java index 44ddabccc4e4..6b5d658845bb 100644 --- a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulHealthProducerInvokeOnHeaderFactory.java +++ b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulHealthProducerInvokeOnHeaderFactory.java @@ -27,7 +27,7 @@ public class ConsulHealthProducerInvokeOnHeaderFactory implements InvokeOnHeader case "SERVICE_CHECKS": target.serviceChecks(exchange.getMessage()); return null; case "service_instances": case "SERVICE_INSTANCES": target.serviceInstances(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulKeyValueProducerInvokeOnHeaderFactory.java b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulKeyValueProducerInvokeOnHeaderFactory.java index c793fcac1c59..7a94bb91a4c9 100644 --- a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulKeyValueProducerInvokeOnHeaderFactory.java +++ b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulKeyValueProducerInvokeOnHeaderFactory.java @@ -37,7 +37,7 @@ public class ConsulKeyValueProducerInvokeOnHeaderFactory implements InvokeOnHead case "PUT": target.put(exchange.getMessage()); return null; case "unlock": case "UNLOCK": target.unlock(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulPreparedQueryProducerInvokeOnHeaderFactory.java b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulPreparedQueryProducerInvokeOnHeaderFactory.java index 5fb2476365c6..92c006f75b3f 100644 --- a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulPreparedQueryProducerInvokeOnHeaderFactory.java +++ b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulPreparedQueryProducerInvokeOnHeaderFactory.java @@ -25,7 +25,7 @@ public class ConsulPreparedQueryProducerInvokeOnHeaderFactory implements InvokeO case "EXECUTE": target.execute(exchange.getMessage()); return null; case "get": case "GET": target.get(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulSessionProducerInvokeOnHeaderFactory.java b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulSessionProducerInvokeOnHeaderFactory.java index a8e843f100b1..30f8801121b3 100644 --- a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulSessionProducerInvokeOnHeaderFactory.java +++ b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulSessionProducerInvokeOnHeaderFactory.java @@ -29,7 +29,7 @@ public class ConsulSessionProducerInvokeOnHeaderFactory implements InvokeOnHeade case "LIST": target.list(exchange.getMessage()); return null; case "renew": case "RENEW": target.renew(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulStatusProducerInvokeOnHeaderFactory.java b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulStatusProducerInvokeOnHeaderFactory.java index e31378c8c9dc..7fcecb1871a4 100644 --- a/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulStatusProducerInvokeOnHeaderFactory.java +++ b/components/camel-consul/src/generated/java/org/apache/camel/component/consul/endpoint/ConsulStatusProducerInvokeOnHeaderFactory.java @@ -23,7 +23,7 @@ public class ConsulStatusProducerInvokeOnHeaderFactory implements InvokeOnHeader case "LEADER": return target.invokeChecks(exchange.getMessage()); case "peers": case "PEERS": return target.invokePeers(exchange.getMessage()); - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlProducerInvokeOnHeaderFactory.java b/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlProducerInvokeOnHeaderFactory.java index 2a3c67c4e962..2ed9e105954b 100644 --- a/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlProducerInvokeOnHeaderFactory.java +++ b/components/camel-dynamic-router/src/generated/java/org/apache/camel/component/dynamicrouter/control/DynamicRouterControlProducerInvokeOnHeaderFactory.java @@ -24,7 +24,7 @@ public class DynamicRouterControlProducerInvokeOnHeaderFactory implements Invoke case "subscribe": target.performSubscribe(exchange.getMessage(), callback); return callback; case "unsubscribe": target.performUnsubscribe(exchange.getMessage(), callback); return callback; case "update": target.performUpdate(exchange.getMessage(), callback); return callback; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-ehcache/src/generated/java/org/apache/camel/component/ehcache/EhcacheProducerInvokeOnHeaderFactory.java b/components/camel-ehcache/src/generated/java/org/apache/camel/component/ehcache/EhcacheProducerInvokeOnHeaderFactory.java index 9a32bdebc865..4aa5d0079e27 100644 --- a/components/camel-ehcache/src/generated/java/org/apache/camel/component/ehcache/EhcacheProducerInvokeOnHeaderFactory.java +++ b/components/camel-ehcache/src/generated/java/org/apache/camel/component/ehcache/EhcacheProducerInvokeOnHeaderFactory.java @@ -37,7 +37,7 @@ public class EhcacheProducerInvokeOnHeaderFactory implements InvokeOnHeaderStrat case "REMOVE_ALL": target.onRemoveAll(exchange.getMessage()); return null; case "replace": case "REPLACE": target.onReplace(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/GoogleSheetsConsumerSplitResultTest.java b/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/GoogleSheetsConsumerSplitResultTest.java new file mode 100644 index 000000000000..d0af95e07cb9 --- /dev/null +++ b/components/camel-google/camel-google-sheets/src/test/java/org/apache/camel/component/google/sheets/GoogleSheetsConsumerSplitResultTest.java @@ -0,0 +1,86 @@ +/* + * 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.google.sheets; + +import java.util.Collection; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.google.api.services.sheets.v4.Sheets; +import org.apache.camel.CamelContext; +import org.apache.camel.impl.DefaultCamelContext; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class GoogleSheetsConsumerSplitResultTest { + + private static final String URI + = "google-sheets://spreadsheets/get?spreadsheetId=abc&clientId=id&clientSecret=secret&refreshToken=token"; + + @Test + void shouldSplitResultByDefault() throws Exception { + try (CamelContext context = createContext()) { + GoogleSheetsConsumer consumer = createConsumer(context, URI); + assertTrue(consumer.isSplitResult()); + } + } + + @Test + void shouldNotSplitResultWhenTurnedOff() throws Exception { + try (CamelContext context = createContext()) { + GoogleSheetsConsumer consumer = createConsumer(context, URI + "&splitResult=false"); + assertFalse(consumer.isSplitResult()); + } + } + + private static CamelContext createContext() { + CamelContext context = new DefaultCamelContext(); + context.getComponent("google-sheets", GoogleSheetsComponent.class).setClientFactory(new TestClientFactory()); + context.start(); + return context; + } + + private static GoogleSheetsConsumer createConsumer(CamelContext context, String uri) throws Exception { + GoogleSheetsEndpoint endpoint = context.getEndpoint(uri, GoogleSheetsEndpoint.class); + return (GoogleSheetsConsumer) endpoint.createConsumer(exchange -> { + }); + } + + private static final class TestClientFactory implements GoogleSheetsClientFactory { + + @Override + public Sheets makeClient( + String clientId, String clientSecret, Collection<String> scopes, String applicationName, + String refreshToken, String accessToken) { + return newClient(); + } + + @Override + public Sheets makeClient( + CamelContext camelContext, String serviceAccountKey, Collection<String> scopes, String applicationName, + String delegate) { + return newClient(); + } + + private static Sheets newClient() { + return new Sheets.Builder(new NetHttpTransport(), new JacksonFactory(), null).setApplicationName("test") + .build(); + } + } +} diff --git a/components/camel-infinispan/camel-infinispan-common/src/generated/java/org/apache/camel/component/infinispan/InfinispanProducerInvokeOnHeaderFactory.java b/components/camel-infinispan/camel-infinispan-common/src/generated/java/org/apache/camel/component/infinispan/InfinispanProducerInvokeOnHeaderFactory.java index 42a218a48f70..4fa90ba5b358 100644 --- a/components/camel-infinispan/camel-infinispan-common/src/generated/java/org/apache/camel/component/infinispan/InfinispanProducerInvokeOnHeaderFactory.java +++ b/components/camel-infinispan/camel-infinispan-common/src/generated/java/org/apache/camel/component/infinispan/InfinispanProducerInvokeOnHeaderFactory.java @@ -57,7 +57,7 @@ public class InfinispanProducerInvokeOnHeaderFactory implements InvokeOnHeaderSt case "REPLACEASYNC": target.onReplaceAsync(exchange.getMessage()); return null; case "size": case "SIZE": target.onSize(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-infinispan/camel-infinispan-embedded/src/generated/java/org/apache/camel/component/infinispan/embedded/InfinispanEmbeddedProducerInvokeOnHeaderFactory.java b/components/camel-infinispan/camel-infinispan-embedded/src/generated/java/org/apache/camel/component/infinispan/embedded/InfinispanEmbeddedProducerInvokeOnHeaderFactory.java index 05d507863615..8e0ee025fe46 100644 --- a/components/camel-infinispan/camel-infinispan-embedded/src/generated/java/org/apache/camel/component/infinispan/embedded/InfinispanEmbeddedProducerInvokeOnHeaderFactory.java +++ b/components/camel-infinispan/camel-infinispan-embedded/src/generated/java/org/apache/camel/component/infinispan/embedded/InfinispanEmbeddedProducerInvokeOnHeaderFactory.java @@ -23,7 +23,7 @@ public class InfinispanEmbeddedProducerInvokeOnHeaderFactory implements InvokeOn case "QUERY": target.onQuery(exchange.getMessage()); return null; case "stats": case "STATS": target.onStats(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-infinispan/camel-infinispan/src/generated/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteProducerInvokeOnHeaderFactory.java b/components/camel-infinispan/camel-infinispan/src/generated/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteProducerInvokeOnHeaderFactory.java index 3281e9823b17..bd0d1ebab0c0 100644 --- a/components/camel-infinispan/camel-infinispan/src/generated/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteProducerInvokeOnHeaderFactory.java +++ b/components/camel-infinispan/camel-infinispan/src/generated/java/org/apache/camel/component/infinispan/remote/InfinispanRemoteProducerInvokeOnHeaderFactory.java @@ -23,7 +23,7 @@ public class InfinispanRemoteProducerInvokeOnHeaderFactory implements InvokeOnHe case "QUERY": target.onQuery(exchange.getMessage()); return null; case "stats": case "STATS": target.onStats(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-servicenow/camel-servicenow-component/src/generated/java/org/apache/camel/component/servicenow/releases/fuji/FujiServiceNowProducerInvokeOnHeaderFactory.java b/components/camel-servicenow/camel-servicenow-component/src/generated/java/org/apache/camel/component/servicenow/releases/fuji/FujiServiceNowProducerInvokeOnHeaderFactory.java index 419cd65b568d..08638ca151e9 100644 --- a/components/camel-servicenow/camel-servicenow-component/src/generated/java/org/apache/camel/component/servicenow/releases/fuji/FujiServiceNowProducerInvokeOnHeaderFactory.java +++ b/components/camel-servicenow/camel-servicenow-component/src/generated/java/org/apache/camel/component/servicenow/releases/fuji/FujiServiceNowProducerInvokeOnHeaderFactory.java @@ -22,7 +22,7 @@ public class FujiServiceNowProducerInvokeOnHeaderFactory implements InvokeOnHead case "aggregate": target.invokeProcessor2(exchange); return null; case "import": target.invokeProcessor3(exchange); return null; case "table": target.invokeProcessor1(exchange); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-servicenow/camel-servicenow-component/src/generated/java/org/apache/camel/component/servicenow/releases/helsinki/HelsinkiServiceNowProducerInvokeOnHeaderFactory.java b/components/camel-servicenow/camel-servicenow-component/src/generated/java/org/apache/camel/component/servicenow/releases/helsinki/HelsinkiServiceNowProducerInvokeOnHeaderFactory.java index 084dfb7f2994..cd2325e5bedb 100644 --- a/components/camel-servicenow/camel-servicenow-component/src/generated/java/org/apache/camel/component/servicenow/releases/helsinki/HelsinkiServiceNowProducerInvokeOnHeaderFactory.java +++ b/components/camel-servicenow/camel-servicenow-component/src/generated/java/org/apache/camel/component/servicenow/releases/helsinki/HelsinkiServiceNowProducerInvokeOnHeaderFactory.java @@ -29,7 +29,7 @@ public class HelsinkiServiceNowProducerInvokeOnHeaderFactory implements InvokeOn case "service_catalog_categories": target.invokeProcessor10(exchange); return null; case "service_catalog_items": target.invokeProcessor8(exchange); return null; case "table": target.invokeProcessor1(exchange); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-spring-parent/camel-spring-redis/src/generated/java/org/apache/camel/component/redis/RedisProducerInvokeOnHeaderFactory.java b/components/camel-spring-parent/camel-spring-redis/src/generated/java/org/apache/camel/component/redis/RedisProducerInvokeOnHeaderFactory.java index 8c5f4f079011..c9e5f6392c41 100644 --- a/components/camel-spring-parent/camel-spring-redis/src/generated/java/org/apache/camel/component/redis/RedisProducerInvokeOnHeaderFactory.java +++ b/components/camel-spring-parent/camel-spring-redis/src/generated/java/org/apache/camel/component/redis/RedisProducerInvokeOnHeaderFactory.java @@ -231,7 +231,7 @@ public class RedisProducerInvokeOnHeaderFactory implements InvokeOnHeaderStrateg case "ZREVRANK": return target.invokeZrevrank(exchange); case "zunionstore": case "ZUNIONSTORE": target.invokeZunionstore(exchange); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/components/camel-web3j/src/generated/java/org/apache/camel/component/web3j/Web3jProducerInvokeOnHeaderFactory.java b/components/camel-web3j/src/generated/java/org/apache/camel/component/web3j/Web3jProducerInvokeOnHeaderFactory.java index 11879df19bcd..a52357fdd6e0 100644 --- a/components/camel-web3j/src/generated/java/org/apache/camel/component/web3j/Web3jProducerInvokeOnHeaderFactory.java +++ b/components/camel-web3j/src/generated/java/org/apache/camel/component/web3j/Web3jProducerInvokeOnHeaderFactory.java @@ -149,7 +149,7 @@ public class Web3jProducerInvokeOnHeaderFactory implements InvokeOnHeaderStrateg case "WEB3_SHA3": target.web3Sha3(exchange.getMessage()); return null; case "ethprotocolversion": case "ethProtocolVersion": target.ethProtocolVersion(exchange.getMessage()); return null; - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } } diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/InvokeOnHeaderStrategy.java b/core/camel-api/src/main/java/org/apache/camel/spi/InvokeOnHeaderStrategy.java index 855d441693e4..ddb247d2890b 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/InvokeOnHeaderStrategy.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/InvokeOnHeaderStrategy.java @@ -30,6 +30,19 @@ import org.jspecify.annotations.Nullable; */ public interface InvokeOnHeaderStrategy { + /** + * Returned by {@link #invoke(Object, String, Exchange, AsyncCallback)} when there is no method for the key, so the + * caller can tell an unknown key from a void method, which returns <tt>null</tt>. + * + * @since 4.24 + */ + Object NO_METHOD = new Object() { + @Override + public String toString() { + return "NO_METHOD"; + } + }; + /** * Invoke the method based on the header key * @@ -38,7 +51,8 @@ public interface InvokeOnHeaderStrategy { * @param exchange the exchange * @param callback the async callback * @return option response from invoking the method, or <tt>null</tt> if the method is void if a value is - * returned, then this value is stored as result on the message body. + * returned, then this value is stored as result on the message body. {@link #NO_METHOD} when + * there is no method for the key. * @throws Exception is thrown if error invoking the method. */ @Nullable diff --git a/core/camel-core/src/test/java/org/apache/camel/support/BodyFactsTest.java b/core/camel-core/src/test/java/org/apache/camel/support/BodyFactsTest.java index 7acf6d0219fd..35b821925785 100644 --- a/core/camel-core/src/test/java/org/apache/camel/support/BodyFactsTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/support/BodyFactsTest.java @@ -19,6 +19,7 @@ package org.apache.camel.support; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -161,6 +162,24 @@ public class BodyFactsTest extends ContextTestSupport { assertEquals(Kind.COLLECTION, BodyFacts.kindOf(List.of(1, 2).iterator())); } + @Test + public void testAMapThatIsIterableIsData() { + // such as the response of solr (a SimpleOrderedMap), which describe calls a Map too + IterableMap map = new IterableMap(); + map.put("id", 1); + + assertEquals(Kind.DATA, BodyFacts.kindOf(map)); + assertTrue(BodyFacts.describe(exchange(map)).contains("a Map (IterableMap)"), BodyFacts.describe(exchange(map))); + } + + private static final class IterableMap extends LinkedHashMap<String, Object> + implements Iterable<Map.Entry<String, Object>> { + @Override + public Iterator<Map.Entry<String, Object>> iterator() { + return entrySet().iterator(); + } + } + @Test public void testTheDataFormatOfAContentType() { assertEquals("json", BodyFacts.dataFormatOf("application/json; charset=UTF-8")); diff --git a/core/camel-core/src/test/java/org/apache/camel/support/HeaderSelectorProducerTest.java b/core/camel-core/src/test/java/org/apache/camel/support/HeaderSelectorProducerTest.java new file mode 100644 index 000000000000..4c8ff585c9e2 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/support/HeaderSelectorProducerTest.java @@ -0,0 +1,208 @@ +/* + * 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.support; + +import org.apache.camel.AsyncCallback; +import org.apache.camel.ContextTestSupport; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.Producer; +import org.apache.camel.spi.InvokeOnHeaderStrategy; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * A producer that picks its method by a header tells an unknown value from a void method: the strategies return + * {@link InvokeOnHeaderStrategy#NO_METHOD} for a value they have no method for. + */ +public class HeaderSelectorProducerTest extends ContextTestSupport { + + private static final String HEADER = "MyAction"; + + @Test + public void testMethodWithResult() throws Exception { + Exchange exchange = process(new ChildProducer(endpoint(), null), "HELLO", "World"); + + assertNull(exchange.getException()); + assertEquals("Hello World", exchange.getMessage().getBody()); + } + + @Test + public void testMethodOfTheParentStrategy() throws Exception { + Exchange exchange = process(new ChildProducer(endpoint(), null), "BASE", "World"); + + assertNull(exchange.getException()); + assertEquals("Base World", exchange.getMessage().getBody()); + } + + @Test + public void testVoidMethodDoesNotRunTheParentStrategy() throws Exception { + Exchange exchange = process(new ChildProducer(endpoint(), null), "VOID", "World"); + + assertNull(exchange.getException()); + assertEquals("World", exchange.getMessage().getBody()); + assertEquals("child", exchange.getMessage().getHeader("ranBy")); + } + + @Test + public void testUnknownValueOfTheHeaderFails() throws Exception { + Exchange exchange = process(new ChildProducer(endpoint(), null), "NOSUCH", "World"); + + IllegalArgumentException e = assertInstanceOf(IllegalArgumentException.class, exchange.getException()); + assertEquals("Unsupported value NOSUCH of header MyAction", e.getMessage()); + assertEquals("World", exchange.getMessage().getBody()); + } + + @Test + public void testUnknownDefaultValueFails() throws Exception { + Exchange exchange = process(new ChildProducer(endpoint(), "NOSUCH"), null, "World"); + + IllegalArgumentException e = assertInstanceOf(IllegalArgumentException.class, exchange.getException()); + assertEquals("Unsupported value NOSUCH configured on the endpoint (header MyAction is not set)", e.getMessage()); + } + + @Test + public void testUnknownValueOfAStrategyFromOlderToolingDoesNothing() throws Exception { + // a strategy generated by older tooling returns null for an unknown value, as for a void method + Exchange exchange = process(new LegacyProducer(endpoint()), "NOSUCH", "World"); + + assertNull(exchange.getException()); + assertEquals("World", exchange.getMessage().getBody()); + } + + private Endpoint endpoint() { + return context.getEndpoint("mock:result"); + } + + private Exchange process(Producer producer, String action, Object body) throws Exception { + ((HeaderSelectorProducer) producer).setCamelContext(context); + producer.start(); + try { + Exchange exchange = new DefaultExchange(context); + if (action != null) { + exchange.getMessage().setHeader(HEADER, action); + } + exchange.getMessage().setBody(body); + producer.process(exchange); + return exchange; + } finally { + producer.stop(); + } + } + + public static class BaseProducer extends HeaderSelectorProducer { + + public BaseProducer(Endpoint endpoint, String defaultValue) { + super(endpoint, HEADER, defaultValue); + } + + public String onBase(Message message) { + return "Base " + message.getBody(); + } + + public void onBaseVoid(Message message) { + message.setHeader("ranBy", "base"); + } + } + + public static class ChildProducer extends BaseProducer { + + public ChildProducer(Endpoint endpoint, String defaultValue) { + super(endpoint, defaultValue); + } + + public String onHello(Message message) { + return "Hello " + message.getBody(); + } + + public void onVoid(Message message) { + message.setHeader("ranBy", "child"); + } + } + + public static class LegacyProducer extends HeaderSelectorProducer { + + public LegacyProducer(Endpoint endpoint) { + super(endpoint, HEADER); + } + + public String onHello(Message message) { + return "Hello " + message.getBody(); + } + } + + /** + * As generated for {@link BaseProducer}. + */ + public static class BaseProducerStrategy implements InvokeOnHeaderStrategy { + + @Override + public Object invoke(Object obj, String key, Exchange exchange, AsyncCallback callback) { + BaseProducer target = (BaseProducer) obj; + switch (key) { + case "BASE": + return target.onBase(exchange.getMessage()); + case "VOID": + target.onBaseVoid(exchange.getMessage()); + return null; + default: + return NO_METHOD; + } + } + } + + /** + * As generated for {@link ChildProducer}. + */ + public static class ChildProducerStrategy implements InvokeOnHeaderStrategy { + + @Override + public Object invoke(Object obj, String key, Exchange exchange, AsyncCallback callback) { + ChildProducer target = (ChildProducer) obj; + switch (key) { + case "HELLO": + return target.onHello(exchange.getMessage()); + case "VOID": + target.onVoid(exchange.getMessage()); + return null; + default: + return NO_METHOD; + } + } + } + + /** + * As generated by tooling older than {@link InvokeOnHeaderStrategy#NO_METHOD}. + */ + public static class LegacyProducerStrategy implements InvokeOnHeaderStrategy { + + @Override + public Object invoke(Object obj, String key, Exchange exchange, AsyncCallback callback) { + LegacyProducer target = (LegacyProducer) obj; + switch (key) { + case "HELLO": + return target.onHello(exchange.getMessage()); + default: + return null; + } + } + } +} diff --git a/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiConfiguration.java b/core/camel-core/src/test/java/org/apache/camel/support/component/AbstractApiConfigurationTest.java similarity index 57% copy from core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiConfiguration.java copy to core/camel-core/src/test/java/org/apache/camel/support/component/AbstractApiConfigurationTest.java index 3a3728b453f3..5ecee79e5cfa 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiConfiguration.java +++ b/core/camel-core/src/test/java/org/apache/camel/support/component/AbstractApiConfigurationTest.java @@ -16,21 +16,15 @@ */ package org.apache.camel.support.component; -import org.apache.camel.spi.UriParam; +import org.junit.jupiter.api.Test; -public class AbstractApiConfiguration { +import static org.junit.jupiter.api.Assertions.assertTrue; - @UriParam(label = "consumer", defaultValue = "true", - description = "When consumer return an array or collection this will generate one exchange per element, and their routes will be executed once for each exchange." - + " Set this value to false to use a single exchange for the entire list or array.") - private boolean splitResult; +public class AbstractApiConfigurationTest { - public boolean isSplitResult() { - return splitResult; + @Test + public void testSplitResultIsTrueByDefault() { + // as documented, and as the consumers of the API components that do not extend this configuration + assertTrue(new AbstractApiConfiguration().isSplitResult()); } - - public void setSplitResult(boolean splitResult) { - this.splitResult = splitResult; - } - } diff --git a/core/camel-core/src/test/java/org/apache/camel/support/component/AbstractApiProducerTest.java b/core/camel-core/src/test/java/org/apache/camel/support/component/AbstractApiProducerTest.java new file mode 100644 index 000000000000..5e9aab2b8761 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/support/component/AbstractApiProducerTest.java @@ -0,0 +1,200 @@ +/* + * 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.support.component; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.camel.CamelContext; +import org.apache.camel.Component; +import org.apache.camel.Consumer; +import org.apache.camel.ContextTestSupport; +import org.apache.camel.Endpoint; +import org.apache.camel.Processor; +import org.apache.camel.Producer; +import org.apache.camel.spi.PropertyConfigurer; +import org.apache.camel.spi.PropertyConfigurerGetter; +import org.apache.camel.support.DefaultComponent; +import org.apache.camel.support.component.ApiMethodHelperTest.TestMethod; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The producer of an API component converts the message body to the type of the parameter that inBody names. + */ +public class AbstractApiProducerTest extends ContextTestSupport { + + private static final ApiMethodHelper<TestMethod> METHOD_HELPER + = new ApiMethodHelper<>(TestMethod.class, Map.of(), List.of()); + + @Test + public void testBodyIsConvertedToTheTypeOfTheInBodyParameter() throws Exception { + Endpoint endpoint = createEndpoint("greetMe", "name"); + + // the parameter is a String, and the endpoint configurer does not know it, as it is not an endpoint option + Object out = template.requestBody(endpoint, "World".getBytes()); + assertEquals("Greetings World", out); + } + + @Test + public void testBodyOfTheTypeOfTheInBodyParameter() throws Exception { + Endpoint endpoint = createEndpoint("greetMe", "name"); + + Object out = template.requestBody(endpoint, "World"); + assertEquals("Greetings World", out); + } + + @Test + public void testBodyIsConvertedToAPrimitiveParameter() throws Exception { + Endpoint endpoint = createEndpoint("greetTimes", "times", Map.of("name", "World")); + + Object out = template.requestBody(endpoint, "2"); + assertTrue(out instanceof String[], "Should be an array: " + out); + assertEquals(2, ((String[]) out).length); + } + + private Endpoint createEndpoint(String methodName, String inBody) throws Exception { + return createEndpoint(methodName, inBody, Map.of()); + } + + private Endpoint createEndpoint(String methodName, String inBody, Map<String, Object> options) throws Exception { + TestApiComponent component = new TestApiComponent(context); + TestApiEndpoint endpoint = new TestApiEndpoint("test-api:" + methodName, component, methodName); + endpoint.setInBody(inBody); + endpoint.configureProperties(new HashMap<>(options)); + context.addEndpoint(endpoint.getEndpointUri(), endpoint); + return endpoint; + } + + enum TestApiName implements ApiName { + TEST; + + @Override + public String getName() { + return "test"; + } + } + + public static class TestConfiguration { + } + + @SuppressWarnings("unused") + public static class TestEndpointConfiguration extends TestConfiguration { + private String name; + private Integer times; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getTimes() { + return times; + } + + public void setTimes(Integer times) { + this.times = times; + } + } + + static class TestApiComponent extends DefaultComponent { + + // as generated for an API component: it knows the options of the endpoint, not the parameters of the methods + private final PropertyConfigurer configurer = new TestEndpointConfigurer(); + + TestApiComponent(CamelContext context) { + super(context); + } + + @Override + protected Endpoint createEndpoint(String uri, String remaining, Map<String, Object> parameters) { + throw new UnsupportedOperationException(); + } + + @Override + public PropertyConfigurer getEndpointPropertyConfigurer() { + return configurer; + } + } + + static class TestEndpointConfigurer implements PropertyConfigurer, PropertyConfigurerGetter { + + @Override + public boolean configure(CamelContext camelContext, Object target, String name, Object value, boolean ignoreCase) { + return false; + } + + @Override + public Class<?> getOptionType(String name, boolean ignoreCase) { + return "inBody".equalsIgnoreCase(name) ? String.class : null; + } + + @Override + public Object getOptionValue(Object target, String name, boolean ignoreCase) { + return null; + } + } + + static class TestApiEndpoint extends AbstractApiEndpoint<TestApiName, TestConfiguration> { + + private final ApiMethodPropertiesHelper<TestConfiguration> propertiesHelper; + + TestApiEndpoint(String uri, Component component, String methodName) { + super(uri, component, TestApiName.TEST, methodName, METHOD_HELPER, new TestEndpointConfiguration()); + this.propertiesHelper = new ApiMethodPropertiesHelper<>( + component.getCamelContext(), TestConfiguration.class, "CamelTestApi.") { + }; + } + + @Override + protected ApiMethodPropertiesHelper<TestConfiguration> getPropertiesHelper() { + return propertiesHelper; + } + + @Override + protected void afterConfigureProperties() { + // noop + } + + @Override + public Object getApiProxy(ApiMethod method, Map<String, Object> args) { + return new TestProxy(); + } + + @Override + protected String getThreadProfileName() { + return "TestApiThreadProfile"; + } + + @Override + public Producer createProducer() { + return new AbstractApiProducer<>(this, propertiesHelper) { + }; + } + + @Override + public Consumer createConsumer(Processor processor) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/core/camel-core/src/test/java/org/apache/camel/support/component/ApiMethodPropertiesHelperTest.java b/core/camel-core/src/test/java/org/apache/camel/support/component/ApiMethodPropertiesHelperTest.java index 5cfac920d081..fe2dd91e5a71 100644 --- a/core/camel-core/src/test/java/org/apache/camel/support/component/ApiMethodPropertiesHelperTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/support/component/ApiMethodPropertiesHelperTest.java @@ -66,6 +66,22 @@ public class ApiMethodPropertiesHelperTest { assertEquals(5, properties.size()); } + @Test + public void testHeaderNamedAsThePrefixIsIgnored() { + final CamelContext camelContext = new DefaultCamelContext(); + MockEndpoint mock = new MockEndpoint(null, new MockComponent(camelContext)); + + final HashMap<String, Object> properties = new HashMap<>(); + final DefaultExchange exchange = DefaultExchange.newFromEndpoint(mock); + exchange.getIn().setHeader(PROPERTY_1, VALUE_1); + // a header named exactly as the camel case prefix, or the prefix, names no property + exchange.getIn().setHeader(TEST_PREFIX.substring(0, TEST_PREFIX.length() - 1), "camelCasePrefix"); + exchange.getIn().setHeader(TEST_PREFIX, "prefix"); + propertiesHelper.getExchangeProperties(exchange, properties); + assertEquals(1, properties.size()); + assertEquals(VALUE_1, properties.get("property1")); + } + @Test public void testGetEndpointProperties() { final HashMap<String, Object> properties = new HashMap<>(); diff --git a/core/camel-core/src/test/resources/META-INF/services/org/apache/camel/invoke-on-header/org.apache.camel.support.HeaderSelectorProducerTest$BaseProducer b/core/camel-core/src/test/resources/META-INF/services/org/apache/camel/invoke-on-header/org.apache.camel.support.HeaderSelectorProducerTest$BaseProducer new file mode 100644 index 000000000000..280a537fbfc9 --- /dev/null +++ b/core/camel-core/src/test/resources/META-INF/services/org/apache/camel/invoke-on-header/org.apache.camel.support.HeaderSelectorProducerTest$BaseProducer @@ -0,0 +1,18 @@ +# +# 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. +# + +class=org.apache.camel.support.HeaderSelectorProducerTest$BaseProducerStrategy diff --git a/core/camel-core/src/test/resources/META-INF/services/org/apache/camel/invoke-on-header/org.apache.camel.support.HeaderSelectorProducerTest$ChildProducer b/core/camel-core/src/test/resources/META-INF/services/org/apache/camel/invoke-on-header/org.apache.camel.support.HeaderSelectorProducerTest$ChildProducer new file mode 100644 index 000000000000..b70e4f29dbdb --- /dev/null +++ b/core/camel-core/src/test/resources/META-INF/services/org/apache/camel/invoke-on-header/org.apache.camel.support.HeaderSelectorProducerTest$ChildProducer @@ -0,0 +1,18 @@ +# +# 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. +# + +class=org.apache.camel.support.HeaderSelectorProducerTest$ChildProducerStrategy diff --git a/core/camel-core/src/test/resources/META-INF/services/org/apache/camel/invoke-on-header/org.apache.camel.support.HeaderSelectorProducerTest$LegacyProducer b/core/camel-core/src/test/resources/META-INF/services/org/apache/camel/invoke-on-header/org.apache.camel.support.HeaderSelectorProducerTest$LegacyProducer new file mode 100644 index 000000000000..0114d35e751c --- /dev/null +++ b/core/camel-core/src/test/resources/META-INF/services/org/apache/camel/invoke-on-header/org.apache.camel.support.HeaderSelectorProducerTest$LegacyProducer @@ -0,0 +1,18 @@ +# +# 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. +# + +class=org.apache.camel.support.HeaderSelectorProducerTest$LegacyProducerStrategy diff --git a/core/camel-support/src/main/java/org/apache/camel/support/BodyFacts.java b/core/camel-support/src/main/java/org/apache/camel/support/BodyFacts.java index 009434e7bc93..ef8584393b57 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/BodyFacts.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/BodyFacts.java @@ -414,6 +414,9 @@ public final class BodyFacts { return BodyTypeRegistry.Kind.NULL; } else if (isPayload(body) || isValue(body)) { return BodyTypeRegistry.Kind.PAYLOAD; + } else if (body instanceof Map<?, ?>) { + // a Map that is also Iterable (such as a SimpleOrderedMap) is data, as describe calls it + return BodyTypeRegistry.Kind.DATA; } else if (body instanceof Collection<?> || body.getClass().isArray() || isIterator(body)) { return BodyTypeRegistry.Kind.COLLECTION; } diff --git a/core/camel-support/src/main/java/org/apache/camel/support/HeaderSelectorProducer.java b/core/camel-support/src/main/java/org/apache/camel/support/HeaderSelectorProducer.java index 5cb617324178..e5891611bde9 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/HeaderSelectorProducer.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/HeaderSelectorProducer.java @@ -33,6 +33,8 @@ import org.slf4j.LoggerFactory; /** * A selector-based producer which uses a header value to determine which processor should be invoked. + * <p/> + * A value that has no method fails the exchange with an {@link IllegalArgumentException}. * * @see org.apache.camel.spi.InvokeOnHeader * @see InvokeOnHeaderStrategy @@ -132,6 +134,7 @@ public abstract class HeaderSelectorProducer extends DefaultAsyncProducer implem try { String header = headerSupplier.get(); String action = exchange.getIn().getHeader(header, String.class); + boolean fromHeader = action != null; if (action == null) { action = defaultHeaderValueSupplier.get(); @@ -142,9 +145,18 @@ public abstract class HeaderSelectorProducer extends DefaultAsyncProducer implem LOG.debug("Invoking @InvokeOnHeader method: {}", action); Object answer = strategy.invoke(target, action, exchange, callback); - if (answer == null && parentStrategy != null) { + if (answer == InvokeOnHeaderStrategy.NO_METHOD && parentStrategy != null) { answer = parentStrategy.invoke(target, action, exchange, callback); } + if (answer == InvokeOnHeaderStrategy.NO_METHOD) { + // a strategy generated by older tooling returns null instead, and nothing happens + if (fromHeader) { + throw new IllegalArgumentException("Unsupported value " + action + " of header " + header); + } else { + throw new IllegalArgumentException( + "Unsupported value " + action + " configured on the endpoint (header " + header + " is not set)"); + } + } if (answer == callback) { // okay it was an async-invoked so we should return false sync = false; diff --git a/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiConfiguration.java b/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiConfiguration.java index 3a3728b453f3..5bb9696e50ad 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiConfiguration.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiConfiguration.java @@ -23,7 +23,7 @@ public class AbstractApiConfiguration { @UriParam(label = "consumer", defaultValue = "true", description = "When consumer return an array or collection this will generate one exchange per element, and their routes will be executed once for each exchange." + " Set this value to false to use a single exchange for the entire list or array.") - private boolean splitResult; + private boolean splitResult = true; public boolean isSplitResult() { return splitResult; diff --git a/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiProducer.java b/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiProducer.java index e59d0a9c5e0a..0780d860b2bb 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiProducer.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/component/AbstractApiProducer.java @@ -16,6 +16,7 @@ */ package org.apache.camel.support.component; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -174,18 +175,22 @@ public abstract class AbstractApiProducer<E extends Enum<E> & ApiName, T> Object value = exchange.getIn().getBody(); if (value != null) { try { + Class<?> type = null; // attempt to find out type via configurer so we avoid using reflection PropertyConfigurer configurer = endpoint.getComponent().getEndpointPropertyConfigurer(); if (configurer instanceof PropertyConfigurerGetter getter) { - Class<?> type = getter.getOptionType(inBodyProperty, true); - if (type != null) { - value = endpoint.getCamelContext().getTypeConverter().mandatoryConvertTo(type, exchange, value); - } - } else { + type = getter.getOptionType(inBodyProperty, true); + } + if (type == null) { + // the configurer knows the options of the endpoint, not the parameters of the API methods + type = methodHelper.allArguments().get(inBodyProperty); + } + if (type == null && !(configurer instanceof PropertyConfigurerGetter)) { // fallback to be reflection based - value = endpoint.getCamelContext().getTypeConverter().mandatoryConvertTo( - endpoint.getConfiguration().getClass().getDeclaredField(inBodyProperty).getType(), - exchange, value); + type = endpoint.getConfiguration().getClass().getDeclaredField(inBodyProperty).getType(); + } + if (type != null && !isArrayValueForArrayType(type, value)) { + value = endpoint.getCamelContext().getTypeConverter().mandatoryConvertTo(type, exchange, value); } } catch (Exception e) { exchange.setException(new RuntimeCamelException( @@ -210,4 +215,9 @@ public abstract class AbstractApiProducer<E extends Enum<E> & ApiName, T> return true; } + + // a collection or an array for an array parameter is converted when the method is invoked + private static boolean isArrayValueForArrayType(Class<?> type, Object value) { + return type.isArray() && (value instanceof Collection<?> || value.getClass().isArray()); + } } diff --git a/core/camel-support/src/main/java/org/apache/camel/support/component/ApiMethodPropertiesHelper.java b/core/camel-support/src/main/java/org/apache/camel/support/component/ApiMethodPropertiesHelper.java index a9dbfb71a7f6..f22a0896f7e2 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/component/ApiMethodPropertiesHelper.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/component/ApiMethodPropertiesHelper.java @@ -88,6 +88,10 @@ public abstract class ApiMethodPropertiesHelper<C> { int nProperties = 0; for (Map.Entry<String, Object> entry : exchange.getIn().getHeaders().entrySet()) { final String key = entry.getKey(); + if (key.length() < prefixLength || key.equals(propertyPrefix)) { + // a header named as the prefix, or as the camel case prefix, names no property + continue; + } if (key.startsWith(propertyPrefix)) { properties.put(key.substring(prefixLength), entry.getValue()); diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index 55758c2545c6..6980f18648e5 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -2598,3 +2598,35 @@ With `crossOriginFilterOn=true`, the CORS responses now include `Access-Control- has an entry with a wildcard such as `https://*` or an entry `null`, the origin is still allowed, but without credentials. Previously credentials were allowed for every origin. A cross-origin client that sends cookies or other credentials must have its origin listed in `allowedOrigins`. + +=== Producers that pick their operation by a header - an unknown value fails + +A producer that picks its method by a header or an option (a `HeaderSelectorProducer`, used by camel-caffeine, +camel-consul, camel-dynamic-router, camel-ehcache, camel-infinispan, camel-servicenow, camel-spring-redis and +camel-web3j) now fails the exchange with an `IllegalArgumentException`, such as +`Unsupported value NOSUCH of header CamelCaffeineAction`, when no method handles the value. Previously the exchange +completed with no error and the body unchanged. + +The SPI `InvokeOnHeaderStrategy` has a new constant `NO_METHOD`, which the strategies generated by the Camel tooling +now return for a key without a method. A strategy generated by older tooling returns null and keeps the old behaviour. +The strategy of a parent class is now tried only for `NO_METHOD`, so a void method no longer runs the parent's +strategy too; a third-party producer with a parent class that has its own strategy must have its strategies +regenerated with the 4.24 tooling. + +=== API components - the inBody body is converted to the type of its parameter + +The API components (camel-as2, camel-box, camel-braintree, camel-dhis2, camel-fhir, camel-google-calendar, +camel-google-drive, camel-google-mail, camel-google-sheets, camel-olingo2, camel-olingo4, camel-twilio and +camel-zendesk) now convert the message body to the type of the method parameter that `inBody` names, for example a +`byte[]`, a stream or a file to a `String`. Previously the body was passed as it was, and a body of another type failed +with `argument type mismatch`; a body that cannot be converted now fails before the call, with an error that names the +property. + +A header named exactly as the prefix of an API component, such as `CamelFhir` or `CamelFhir.`, is now ignored. +Previously it failed every call with a `StringIndexOutOfBoundsException`. + +=== camel-google-sheets - splitResult is true by default + +The `splitResult` option of the google-sheets consumer now defaults to `true`, as documented and as the other API +components do: an array or collection result gives one exchange per element. Set `splitResult=false` for one exchange +with the whole result. diff --git a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/invoke-on-header.vm b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/invoke-on-header.vm index d23e0b52f42c..758c244084e4 100644 --- a/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/invoke-on-header.vm +++ b/tooling/maven/camel-package-maven-plugin/src/main/resources/velocity/invoke-on-header.vm @@ -61,7 +61,7 @@ public class ${cn} implements InvokeOnHeaderStrategy { case "${option.key}": return target.${option.methodName}(${args}); #end #end - default: return null; + default: return InvokeOnHeaderStrategy.NO_METHOD; } #end }
