This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push: new 62f3378470c CAMEL-21817: camel-bean: add support for invoking methods with vararg… (#17318) 62f3378470c is described below commit 62f3378470cfd47b6041b02c54234e646f8b835a Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Sat Mar 1 11:28:22 2025 +0000 CAMEL-21817: camel-bean: add support for invoking methods with vararg… (#17318) * CAMEL-21817: camel-bean: add support for invoking methods with vararg parameters --- .../org/apache/camel/component/bean/BeanInfo.java | 9 +- .../apache/camel/component/bean/MethodInfo.java | 139 ++++++++++++++------- .../apache/camel/component/bean/ParameterInfo.java | 9 +- .../component/bean/BeanParameterInfoTest.java | 2 +- .../component/bean/BeanVarargsInject4Test.java | 61 +++++++++ .../component/bean/BeanVarargsInject5Test.java | 62 +++++++++ .../component/bean/BeanVarargsInject6Test.java | 64 ++++++++++ .../component/bean/BeanVarargsInject7Test.java | 67 ++++++++++ 8 files changed, 362 insertions(+), 51 deletions(-) diff --git a/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java b/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java index cc0402c9510..aa8e1d9e0f0 100644 --- a/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java +++ b/components/camel-bean/src/main/java/org/apache/camel/component/bean/BeanInfo.java @@ -286,7 +286,7 @@ public class BeanInfo { // and therefore use arrayLength from ObjectHelper to return the array length field. Method method = org.apache.camel.util.ObjectHelper.class.getMethod("arrayLength", Object[].class); ParameterInfo pi = new ParameterInfo( - 0, Object[].class, null, ExpressionBuilder.mandatoryBodyExpression(Object[].class, true)); + 0, Object[].class, false, null, ExpressionBuilder.mandatoryBodyExpression(Object[].class, true)); List<ParameterInfo> lpi = new ArrayList<>(1); lpi.add(pi); methodInfo = new MethodInfo(exchange.getContext(), pojo.getClass(), method, lpi, lpi, false, false); @@ -447,6 +447,7 @@ public class BeanInfo { boolean hasHandlerAnnotation = org.apache.camel.util.ObjectHelper.hasAnnotation(method.getAnnotations(), Handler.class); int size = parameterTypes.length; + if (LOG.isTraceEnabled()) { LOG.trace("Creating MethodInfo for class: {} method: {} having {} parameters", clazz, method, size); } @@ -457,13 +458,15 @@ public class BeanInfo { = parametersAnnotations[i].toArray(new Annotation[0]); Expression expression = createParameterUnmarshalExpression(method, parameterType, parameterAnnotations); hasCustomAnnotation |= expression != null; + // whether this parameter is vararg which must be last parameter + boolean varargs = method.isVarArgs() && i == size - 1; - ParameterInfo parameterInfo = new ParameterInfo(i, parameterType, parameterAnnotations, expression); + ParameterInfo parameterInfo = new ParameterInfo(i, parameterType, varargs, parameterAnnotations, expression); LOG.trace("Parameter #{}: {}", i, parameterInfo); parameters.add(parameterInfo); if (expression == null) { boolean bodyAnnotation = org.apache.camel.util.ObjectHelper.hasAnnotation(parameterAnnotations, Body.class); - LOG.trace("Parameter #{} has @Body annotation", i); + LOG.trace("Parameter #{} has @Body annotation: {}", i, bodyAnnotation); hasCustomAnnotation |= bodyAnnotation; if (bodyParameters.isEmpty()) { // okay we have not yet set the body parameter and we have found diff --git a/components/camel-bean/src/main/java/org/apache/camel/component/bean/MethodInfo.java b/components/camel-bean/src/main/java/org/apache/camel/component/bean/MethodInfo.java index 1d601e5cec6..6b553fcd073 100644 --- a/components/camel-bean/src/main/java/org/apache/camel/component/bean/MethodInfo.java +++ b/components/camel-bean/src/main/java/org/apache/camel/component/bean/MethodInfo.java @@ -21,6 +21,7 @@ import java.lang.reflect.AccessibleObject; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; @@ -608,31 +609,36 @@ public class MethodInfo { * Evaluates all the parameter expressions */ private Object[] evaluateParameterExpressions(Exchange exchange, Object body, Iterator<?> it) { - Object[] answer = new Object[expressions.length]; - for (int i = 0; i < expressions.length; i++) { - + Object[] answer = new Object[expressions != null ? expressions.length : 1]; + for (int i = 0; expressions == null || i < expressions.length; i++) { if (body instanceof StreamCache) { // need to reset stream cache for each expression as you may access the message body in multiple parameters ((StreamCache) body).reset(); } - // grab the parameter value for the given index - Object parameterValue = it != null && it.hasNext() ? it.next() : null; + // whether its vararg + boolean varargs = parameters.get(i).isVarargs(); // and the expected parameter type Class<?> parameterType = parameters.get(i).getType(); // the value for the parameter to use Object value = null; - // prefer to use parameter value if given, as they override any bean parameter binding - // we should skip * as its a type placeholder to indicate any type - if (parameterValue != null && !parameterValue.equals("*")) { - // evaluate the parameter value binding - value = evaluateParameterValue(exchange, i, parameterValue, parameterType); + if (varargs) { + value = evaluateVarargsParameterExpressions(exchange, body, it); + } else { + // grab the parameter value for the given index + Object parameterValue = it != null && it.hasNext() ? it.next() : null; + // prefer to use parameter value if given, as they override any bean parameter binding + // we should skip * as its a type placeholder to indicate any type + if (parameterValue != null && !parameterValue.equals("*")) { + // evaluate the parameter value binding + value = evaluateParameterValue(exchange, i, parameterValue, parameterType, false); + } } // use bean parameter binding, if still no value - Expression expression = expressions[i]; + Expression expression = expressions != null ? expressions[i] : null; if (value == null && expression != null) { - value = evaluateParameterBinding(exchange, expression, i, parameterType); + value = evaluateParameterBinding(exchange, expression, i, parameterType, false); } // remember the value to use if (value != Void.TYPE) { @@ -643,6 +649,35 @@ public class MethodInfo { return answer; } + /** + * Evaluate the remainder parameter as a single vararg + */ + private Object evaluateVarargsParameterExpressions(Exchange exchange, Object body, Iterator<?> it) { + // special for varargs + if (body instanceof StreamCache) { + // need to reset stream cache for each expression as you may access the message body in multiple parameters + ((StreamCache) body).reset(); + } + List<Object> answer = new ArrayList<>(); + int i = 0; + while (it.hasNext()) { + Object parameterValue = it.next(); + Object value = null; + // prefer to use parameter value if given, as they override any bean parameter binding + // we should skip * as its a type placeholder to indicate any type + if (parameterValue != null && !parameterValue.equals("*")) { + // evaluate the parameter value binding + value = evaluateParameterValue(exchange, i, parameterValue, Object.class, true); + } + // remember the value to use + if (value != Void.TYPE) { + answer.add(value); + } + i++; + } + return answer.toArray(new Object[0]); + } + /** * Evaluate using parameter values where the values can be provided in the method name syntax. * <p/> @@ -654,7 +689,9 @@ public class MethodInfo { * <li>a non <tt>null</tt> value - if the parameter was a parameter value, and to be used</li> * </ul> */ - private Object evaluateParameterValue(Exchange exchange, int index, Object parameterValue, Class<?> parameterType) { + private Object evaluateParameterValue( + Exchange exchange, int index, Object parameterValue, Class<?> parameterType, + boolean varargs) { Object answer = null; // convert the parameter value to a String @@ -669,7 +706,7 @@ public class MethodInfo { // check if its a valid parameter value (no type declared via .class syntax) valid = BeanHelper.isValidParameterValue(exp); - if (!valid) { + if (!valid && !varargs) { // it may be a parameter type instead, and if so, then we should return null, // as this method is only for evaluating parameter values Boolean isClass = BeanHelper.isAssignableToExpectedType(exchange.getContext().getClassResolver(), exp, @@ -703,7 +740,7 @@ public class MethodInfo { } // the parameter value may match the expected type, then we use it as-is - if (parameterType.isAssignableFrom(parameterValue.getClass())) { + if (varargs || parameterType.isAssignableFrom(parameterValue.getClass())) { valid = true; } else { // String values from the simple language is always valid @@ -724,20 +761,25 @@ public class MethodInfo { if (parameterValue instanceof String) { parameterValue = StringHelper.removeLeadingAndEndingQuotes((String) parameterValue); } - try { - // it is a valid parameter value, so convert it to the expected type of the parameter - answer = exchange.getContext().getTypeConverter().mandatoryConvertTo(parameterType, exchange, - parameterValue); - if (LOG.isTraceEnabled()) { - LOG.trace("Parameter #{} evaluated as: {} type: {}", index, answer, - org.apache.camel.util.ObjectHelper.type(answer)); - } - } catch (Exception e) { - if (LOG.isDebugEnabled()) { - LOG.debug("Cannot convert from type: {} to type: {} for parameter #{}", - org.apache.camel.util.ObjectHelper.type(parameterValue), parameterType, index); + if (varargs) { + // use the value as-is + answer = parameterValue; + } else { + try { + // it is a valid parameter value, so convert it to the expected type of the parameter + answer = exchange.getContext().getTypeConverter().mandatoryConvertTo(parameterType, exchange, + parameterValue); + if (LOG.isTraceEnabled()) { + LOG.trace("Parameter #{} evaluated as: {} type: {}", index, answer, + org.apache.camel.util.ObjectHelper.type(answer)); + } + } catch (Exception e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Cannot convert from type: {} to type: {} for parameter #{}", + org.apache.camel.util.ObjectHelper.type(parameterValue), parameterType, index); + } + throw new ParameterBindingException(e, method, index, parameterType, parameterValue); } - throw new ParameterBindingException(e, method, index, parameterType, parameterValue); } } } @@ -748,30 +790,35 @@ public class MethodInfo { /** * Evaluate using classic parameter binding using the pre compute expression */ - private Object evaluateParameterBinding(Exchange exchange, Expression expression, int index, Class<?> parameterType) { + private Object evaluateParameterBinding( + Exchange exchange, Expression expression, int index, Class<?> parameterType, boolean varargs) { Object answer = null; // use object first to avoid type conversion so we know if there is a value or not Object result = expression.evaluate(exchange, Object.class); if (result != null) { - try { - if (parameterType.isInstance(result)) { - // optimize if the value is already the same type - answer = result; - } else { - // we got a value now try to convert it to the expected type - answer = exchange.getContext().getTypeConverter().mandatoryConvertTo(parameterType, result); - } - if (LOG.isTraceEnabled()) { - LOG.trace("Parameter #{} evaluated as: {} type: {}", index, answer, - org.apache.camel.util.ObjectHelper.type(answer)); - } - } catch (NoTypeConversionAvailableException e) { - if (LOG.isDebugEnabled()) { - LOG.debug("Cannot convert from type: {} to type: {} for parameter #{}", - org.apache.camel.util.ObjectHelper.type(result), parameterType, index); + if (varargs) { + answer = result; + } else { + try { + if (parameterType.isInstance(result)) { + // optimize if the value is already the same type + answer = result; + } else { + // we got a value now try to convert it to the expected type + answer = exchange.getContext().getTypeConverter().mandatoryConvertTo(parameterType, result); + } + if (LOG.isTraceEnabled()) { + LOG.trace("Parameter #{} evaluated as: {} type: {}", index, answer, + org.apache.camel.util.ObjectHelper.type(answer)); + } + } catch (NoTypeConversionAvailableException e) { + if (LOG.isDebugEnabled()) { + LOG.debug("Cannot convert from type: {} to type: {} for parameter #{}", + org.apache.camel.util.ObjectHelper.type(result), parameterType, index); + } + throw new ParameterBindingException(e, method, index, parameterType, result); } - throw new ParameterBindingException(e, method, index, parameterType, result); } } else { LOG.trace("Parameter #{} evaluated as null", index); diff --git a/components/camel-bean/src/main/java/org/apache/camel/component/bean/ParameterInfo.java b/components/camel-bean/src/main/java/org/apache/camel/component/bean/ParameterInfo.java index 7a593f4da1d..a3d14f21a85 100644 --- a/components/camel-bean/src/main/java/org/apache/camel/component/bean/ParameterInfo.java +++ b/components/camel-bean/src/main/java/org/apache/camel/component/bean/ParameterInfo.java @@ -28,12 +28,14 @@ final class ParameterInfo { private final int index; private final Class<?> type; + private final boolean varargs; private final Annotation[] annotations; private Expression expression; - ParameterInfo(int index, Class<?> type, Annotation[] annotations, Expression expression) { + ParameterInfo(int index, Class<?> type, boolean varargs, Annotation[] annotations, Expression expression) { this.index = index; this.type = type; + this.varargs = varargs; this.annotations = annotations; this.expression = expression; } @@ -54,6 +56,10 @@ final class ParameterInfo { return type; } + public boolean isVarargs() { + return varargs; + } + public void setExpression(Expression expression) { this.expression = expression; } @@ -64,6 +70,7 @@ final class ParameterInfo { sb.append("ParameterInfo"); sb.append("[index=").append(index); sb.append(", type=").append(type); + sb.append(", varargs=").append(varargs); sb.append(", annotations=").append(annotations == null ? "null" : Arrays.asList(annotations).toString()); sb.append(", expression=").append(expression); sb.append(']'); diff --git a/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanParameterInfoTest.java b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanParameterInfoTest.java index c6b1e3b1e44..03373ff0c68 100644 --- a/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanParameterInfoTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanParameterInfoTest.java @@ -31,7 +31,7 @@ public class BeanParameterInfoTest extends ContextTestSupport { @Test public void testMethodPatternUsingMethodAnnotations() { Class<?> foo = Foo.class.getClass(); - ParameterInfo info = new ParameterInfo(1, foo.getClass(), foo.getAnnotations(), null); + ParameterInfo info = new ParameterInfo(1, foo.getClass(), false, foo.getAnnotations(), null); assertNotNull(info); assertNotNull(info.toString()); diff --git a/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanVarargsInject4Test.java b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanVarargsInject4Test.java new file mode 100644 index 00000000000..b4d13dd5100 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanVarargsInject4Test.java @@ -0,0 +1,61 @@ +/* + * 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.bean; + +import org.apache.camel.ContextTestSupport; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.spi.Registry; +import org.junit.jupiter.api.Test; + +public class BeanVarargsInject4Test extends ContextTestSupport { + + private final MyBean myBean = new MyBean(); + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + public void configure() { + from("direct:start").to("bean:myBean?method=doSomething(1,2,3)").to("mock:finish"); + } + }; + } + + @Test + public void testVarargs() throws Exception { + MockEndpoint end = getMockEndpoint("mock:finish"); + end.expectedBodiesReceived("Bye with 3 args"); + + sendBody("direct:start", "Camel"); + + assertMockEndpointsSatisfied(); + } + + @Override + protected Registry createCamelRegistry() throws Exception { + Registry answer = super.createCamelRegistry(); + answer.bind("myBean", myBean); + return answer; + } + + public static class MyBean { + + public String doSomething(Object... args) { + return "Bye with " + args.length + " args"; + } + } +} diff --git a/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanVarargsInject5Test.java b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanVarargsInject5Test.java new file mode 100644 index 00000000000..4cd394fa0c6 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanVarargsInject5Test.java @@ -0,0 +1,62 @@ +/* + * 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.bean; + +import org.apache.camel.Body; +import org.apache.camel.ContextTestSupport; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.spi.Registry; +import org.junit.jupiter.api.Test; + +public class BeanVarargsInject5Test extends ContextTestSupport { + + private final MyBean myBean = new MyBean(); + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + public void configure() { + from("direct:start").to("bean:myBean?method=doSomething(*,1,2,3)").to("mock:finish"); + } + }; + } + + @Test + public void testVarargs() throws Exception { + MockEndpoint end = getMockEndpoint("mock:finish"); + end.expectedBodiesReceived("Bye Camel with 3 args"); + + sendBody("direct:start", "Camel"); + + assertMockEndpointsSatisfied(); + } + + @Override + protected Registry createCamelRegistry() throws Exception { + Registry answer = super.createCamelRegistry(); + answer.bind("myBean", myBean); + return answer; + } + + public static class MyBean { + + public String doSomething(@Body String body, Object... args) { + return "Bye " + body + " with " + args.length + " args"; + } + } +} diff --git a/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanVarargsInject6Test.java b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanVarargsInject6Test.java new file mode 100644 index 00000000000..632cb56e144 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanVarargsInject6Test.java @@ -0,0 +1,64 @@ +/* + * 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.bean; + +import org.apache.camel.ContextTestSupport; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.spi.Registry; +import org.junit.jupiter.api.Test; + +public class BeanVarargsInject6Test extends ContextTestSupport { + + private final MyBean myBean = new MyBean(); + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + public void configure() { + from("direct:start").to("bean:myBean?method=doSomething(${body},1,2,3)").to("mock:finish"); + } + }; + } + + @Test + public void testVarargs() throws Exception { + MockEndpoint end = getMockEndpoint("mock:finish"); + end.expectedBodiesReceived("Bye Camel with 3 args (sum=6)"); + + sendBody("direct:start", "Camel"); + + assertMockEndpointsSatisfied(); + } + + @Override + protected Registry createCamelRegistry() throws Exception { + Registry answer = super.createCamelRegistry(); + answer.bind("myBean", myBean); + return answer; + } + + public static class MyBean { + + public String doSomething(String body, Object... args) { + int sum = Integer.parseInt((String) args[0]); + sum += Integer.parseInt((String) args[1]); + sum += Integer.parseInt((String) args[2]); + return "Bye " + body + " with " + args.length + " args (sum=" + sum + ")"; + } + } +} diff --git a/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanVarargsInject7Test.java b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanVarargsInject7Test.java new file mode 100644 index 00000000000..dae818a7950 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/component/bean/BeanVarargsInject7Test.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.bean; + +import java.util.Map; + +import org.apache.camel.ContextTestSupport; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.spi.Registry; +import org.junit.jupiter.api.Test; + +public class BeanVarargsInject7Test extends ContextTestSupport { + + private final MyBean myBean = new MyBean(); + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + public void configure() { + from("direct:start").to("bean:myBean?method=doSomething(${header.myArr},1,2,3)").to("mock:finish"); + } + }; + } + + @Test + public void testVarargs() throws Exception { + MockEndpoint end = getMockEndpoint("mock:finish"); + end.expectedBodiesReceived("Bye Camel with 3 args (sum=6)"); + + Object[] arr = new Object[] { "Cam", "el" }; + sendBody("direct:start", "World", Map.of("myArr", arr)); + + assertMockEndpointsSatisfied(); + } + + @Override + protected Registry createCamelRegistry() throws Exception { + Registry answer = super.createCamelRegistry(); + answer.bind("myBean", myBean); + return answer; + } + + public static class MyBean { + + public String doSomething(Object[] arr, Object... args) { + int sum = Integer.parseInt((String) args[0]); + sum += Integer.parseInt((String) args[1]); + sum += Integer.parseInt((String) args[2]); + return "Bye " + arr[0] + arr[1] + " with " + args.length + " args (sum=" + sum + ")"; + } + } +}