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 08a581a CAMEL-16394: Route Template local beans. 08a581a is described below commit 08a581a85cee980aa5c9244f64f42ebc2136118b Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Fri May 7 07:26:43 2021 +0200 CAMEL-16394: Route Template local beans. --- .../component/kamelet/KameletLocalBeanTest.java | 91 ++++++++++++++++++++++ .../org/apache/camel/RouteTemplateContext.java | 12 +++ .../org/apache/camel/impl/DefaultCamelContext.java | 34 ++++++-- .../java/org/apache/camel/impl/DefaultModel.java | 28 +------ .../camel/builder/TemplatedRouteBuilder.java | 3 +- .../camel/model/DefaultRouteTemplateContext.java | 12 +++ .../modules/ROOT/pages/route-template.adoc | 16 ++-- 7 files changed, 158 insertions(+), 38 deletions(-) diff --git a/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletLocalBeanTest.java b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletLocalBeanTest.java new file mode 100644 index 0000000..3e72efd --- /dev/null +++ b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletLocalBeanTest.java @@ -0,0 +1,91 @@ +/* + * 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.kamelet; + +import org.apache.camel.RoutesBuilder; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.test.junit5.CamelTestSupport; +import org.apache.http.annotation.Obsolete; +import org.junit.jupiter.api.Test; + +public class KameletLocalBeanTest extends CamelTestSupport { + + @Test + public void testOne() throws Exception { + getMockEndpoint("mock:result").expectedBodiesReceived("Hi John we are going to Moes"); + + template.sendBody("direct:moe", "John"); + + assertMockEndpointsSatisfied(); + } + + @Test + public void testTwo() throws Exception { + getMockEndpoint("mock:result").expectedBodiesReceived("Hi Jack we are going to Shamrock", + "Hi Mary we are going to Moes"); + + template.sendBody("direct:shamrock", "Jack"); + template.sendBody("direct:moe", "Mary"); + + assertMockEndpointsSatisfied(); + } + + // ********************************************** + // + // test set-up + // + // ********************************************** + + @Obsolete + protected RoutesBuilder createRouteBuilder() throws Exception { + return new RouteBuilder() { + @Override + public void configure() throws Exception { + routeTemplate("whereTo") + .templateParameter("bar") // name of bar + .configure(rtc -> { + // create local bean with id myBar which can be used in the routes via {{myBar}} + rtc.bind("myBar", MyBar.class, new MyBar(rtc.getProperty("bar", String.class))); + }) + .from("kamelet:source") + // must use {{myBar}} to refer to the local bean + .to("bean:{{myBar}}"); + + from("direct:shamrock") + .kamelet("whereTo?bar=Shamrock") + .to("mock:result"); + + from("direct:moe") + .kamelet("whereTo?bar=Moes") + .to("mock:result"); + } + }; + } + + private static class MyBar { + + private final String bar; + + public MyBar(String bar) { + this.bar = bar; + } + + public String where(String name) { + return "Hi " + name + " we are going to " + bar; + } + } +} diff --git a/core/camel-api/src/main/java/org/apache/camel/RouteTemplateContext.java b/core/camel-api/src/main/java/org/apache/camel/RouteTemplateContext.java index 80c3a5a..b03993f 100644 --- a/core/camel-api/src/main/java/org/apache/camel/RouteTemplateContext.java +++ b/core/camel-api/src/main/java/org/apache/camel/RouteTemplateContext.java @@ -17,6 +17,7 @@ package org.apache.camel; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Supplier; import org.apache.camel.spi.BeanRepository; @@ -118,4 +119,15 @@ public interface RouteTemplateContext extends HasCamelContext { * Gets the local bean repository for the route template when creating the new route */ BeanRepository getLocalBeanRepository(); + + /** + * Sets a configurer which allows to do configuration while the route template is being used to create a route. This + * gives control over the creating process, such as binding local beans and doing other kind of customization. + * + * @param configurer the configurer with callback to invoke with the given route template context + */ + void setConfigurer(Consumer<RouteTemplateContext> configurer); + + Consumer<RouteTemplateContext> getConfigurer(); + } diff --git a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultCamelContext.java b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultCamelContext.java index a5d7e1b..20a7a1f 100644 --- a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultCamelContext.java +++ b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultCamelContext.java @@ -751,19 +751,23 @@ public class DefaultCamelContext extends SimpleCamelContext implements ModelCame if (routeDefinition.isTemplate() != null && routeDefinition.isTemplate() && routeDefinition.getTemplateParameters() != null) { + // apply configurer if any present + if (routeDefinition.getRouteTemplateContext().getConfigurer() != null) { + routeDefinition.getRouteTemplateContext().getConfigurer() + .accept(routeDefinition.getRouteTemplateContext()); + } + // copy parameters/bean repository to not cause side-effect Map<String, Object> params = new HashMap<>(routeDefinition.getTemplateParameters()); LocalBeanRegistry bbr = (LocalBeanRegistry) routeDefinition.getRouteTemplateContext().getLocalBeanRepository(); - if (bbr != null) { - bbr = bbr.copy(); - } + LocalBeanRegistry bbrCopy = new LocalBeanRegistry(); // make all bean in the bean repository use unique keys (need to add uuid counter) // so when the route template is used again to create another route, then there is // no side-effect from previously used values that Camel may use in its endpoint // registry and elsewhere - if (bbr != null) { + if (bbr != null && !bbr.isEmpty()) { for (Map.Entry<String, Object> param : params.entrySet()) { Object value = param.getValue(); if (value instanceof String) { @@ -774,11 +778,27 @@ public class DefaultCamelContext extends SimpleCamelContext implements ModelCame LOG.debug( "Route: {} re-assigning local-bean id: {} to: {} to ensure ids are globally unique", routeDefinition.getId(), oldKey, newKey); - bbr.swapKey(oldKey, newKey); + bbrCopy.put(newKey, bbr.remove(oldKey)); param.setValue(newKey); } } } + // the remainder of the local beans must also have their ids made global unique + for (String oldKey : bbr.keySet()) { + String newKey = oldKey + "-" + UUID.generateUuid(); + LOG.debug( + "Route: {} re-assigning local-bean id: {} to: {} to ensure ids are globally unique", + routeDefinition.getId(), oldKey, newKey); + bbrCopy.put(newKey, bbr.get(oldKey)); + if (!params.containsKey(oldKey)) { + // if a bean was bound as local bean with a key and it was not defined as template parameter + // then store it as if it was a template parameter with same key=value which allows us + // to use this local bean in the route without any problem such as: + // to("bean:{{myBean}}") + // and myBean is the local bean id. + params.put(oldKey, newKey); + } + } } Properties prop = new Properties(); @@ -786,8 +806,8 @@ public class DefaultCamelContext extends SimpleCamelContext implements ModelCame pc.setLocalProperties(prop); // we need to shadow the bean registry on the CamelContext with the local beans from the route template context - if (localBeans != null && bbr != null) { - localBeans.setLocalBeanRepository(bbr); + if (localBeans != null && bbrCopy != null) { + localBeans.setLocalBeanRepository(bbrCopy); } // need to reset auto assigned ids, so there is no clash when creating routes diff --git a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java index 7b727a8..5c9e751 100644 --- a/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java +++ b/core/camel-core-engine/src/main/java/org/apache/camel/impl/DefaultModel.java @@ -22,7 +22,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.StringJoiner; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -274,29 +273,6 @@ public class DefaultModel implements Model { prop.putAll(routeTemplateContext.getParameters()); } - // TODO: not sure if this is the best location where this should happen, eventually this can - // be moved at a later stage in the camel context but would mean that we need to add the - // configurer to the route definition. - try { - Properties localProps = new Properties(); - localProps.putAll(prop); - - getCamelContext().getPropertiesComponent().setLocalProperties(localProps); - - // apply configurer if any present - if (target.getConfigurer() != null) { - target.getConfigurer().accept(routeTemplateContext); - } - } finally { - getCamelContext().getPropertiesComponent().setLocalProperties(null); - } - - // override with user parameters part 2: user may have added parameter - // in the customize callback - if (routeTemplateContext.getParameters() != null) { - prop.putAll(routeTemplateContext.getParameters()); - } - RouteTemplateDefinition.Converter converter = RouteTemplateDefinition.Converter.DEFAULT_CONVERTER; for (Map.Entry<String, RouteTemplateDefinition.Converter> entry : routeTemplateConverters.entrySet()) { @@ -322,6 +298,10 @@ public class DefaultModel implements Model { def.setTemplateParameters(prop); def.setRouteTemplateContext(routeTemplateContext); + if (target.getConfigurer() != null) { + routeTemplateContext.setConfigurer(target.getConfigurer()); + } + // assign ids to the routes and validate that the id's are all unique String duplicate = RouteDefinitionHelper.validateUniqueIds(def, routeDefinitions); if (duplicate != null) { diff --git a/core/camel-core-model/src/main/java/org/apache/camel/builder/TemplatedRouteBuilder.java b/core/camel-core-model/src/main/java/org/apache/camel/builder/TemplatedRouteBuilder.java index 05a31eb..fc43ad6 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/builder/TemplatedRouteBuilder.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/builder/TemplatedRouteBuilder.java @@ -161,8 +161,9 @@ public final class TemplatedRouteBuilder { } handler.accept(def); } + // configurer is executed later controlled by the route template context if (configurer != null) { - configurer.accept(routeTemplateContext); + routeTemplateContext.setConfigurer(configurer); } return camelContext.addRouteFromTemplate(routeId, routeTemplateId, routeTemplateContext); } catch (Exception e) { diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/DefaultRouteTemplateContext.java b/core/camel-core-model/src/main/java/org/apache/camel/model/DefaultRouteTemplateContext.java index f77cc5a..e5b490b 100644 --- a/core/camel-core-model/src/main/java/org/apache/camel/model/DefaultRouteTemplateContext.java +++ b/core/camel-core-model/src/main/java/org/apache/camel/model/DefaultRouteTemplateContext.java @@ -18,6 +18,7 @@ package org.apache.camel.model; import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Supplier; import javax.xml.bind.annotation.XmlTransient; @@ -36,6 +37,7 @@ public final class DefaultRouteTemplateContext implements RouteTemplateContext { private final CamelContext camelContext; private final LocalBeanRegistry registry; private final Map<String, Object> parameters; + private Consumer<RouteTemplateContext> configurer; public DefaultRouteTemplateContext(CamelContext camelContext) { this.camelContext = camelContext; @@ -95,4 +97,14 @@ public final class DefaultRouteTemplateContext implements RouteTemplateContext { public BeanRepository getLocalBeanRepository() { return registry; } + + @Override + public Consumer<RouteTemplateContext> getConfigurer() { + return configurer; + } + + @Override + public void setConfigurer(Consumer<RouteTemplateContext> configurer) { + this.configurer = configurer; + } } diff --git a/docs/user-manual/modules/ROOT/pages/route-template.adoc b/docs/user-manual/modules/ROOT/pages/route-template.adoc index 76cebe6..070168c 100644 --- a/docs/user-manual/modules/ROOT/pages/route-template.adoc +++ b/docs/user-manual/modules/ROOT/pages/route-template.adoc @@ -121,8 +121,6 @@ TemplatedRouteBuilder.builder(context, "myTemplate") The route template allows to bind beans which is local scoped and only used as part of creating routes from the template. This allows to use the same template to create multiple routes, where beans are local (private) for each created route. -NOTE: Binding beans to route template is currently only available with Java DSL / Java coding. - For example given the following route template, then we can configure the bindings (and other things) as shown: [source,java] @@ -131,14 +129,16 @@ routeTemplate("s3template") .templateParameter("region") .templateParameter("bucket") .configure((RouteTemplateContext rtc) -> + // name of local bean is myClient rtc.bind("myClient", S3Client.class, S3Client.builder() .region(rtc.getProperty("region", Region.class)) - .build() + .build(); ) ) .from("direct:s3-store") - .to("aws2-s3:{{bucket}}?amazonS3Client=#myClient") + // must refer to the bean with {{myClient}} + .to("aws2-s3:{{bucket}}?amazonS3Client=#{{myClient}}") ---- The template has two parameters to specify the AWS region and the S3 bucket. To connect to S3 @@ -150,6 +150,10 @@ specify `S3Client.class` as parameter. This ensures that the code creating the bean is executed later when Camel is creating a route from the template. This makes the bean with id `myClient` a local bean that is only in the scope (namespace) in the route template. +*Important:* the local bean with id `myClient` *must* be referred to using Camel's property placeholder syntax, eg `{{myClient}}` +in the route template, as shown above with the _to_ endpoint. This is because the local +bean must be made unique and Camel will internally re-assign the bean id to use an unique id instead of `myClient`. And this is done with the help +of the property placeholder functionality. If multiple routes is created from this template, then each of the created routes, have their own S3Client bean created. @@ -168,13 +172,13 @@ Suppose the route template below is defined in XML: <templateParameter name="bucket"/> <route> <from uri="direct:s3-store"/> - <to uri="aws2-s3:{{bucket}}?amazonS3Client=#myClient"/> + <to uri="aws2-s3:{{bucket}}?amazonS3Client=#{{myClient}}"/> </route> </routeTemplate> </camelContext> ---- -The template has no bean bindings for `#myClient` which would be required for creating the template. +The template has no bean bindings for `#{{myClient}}` which would be required for creating the template. When creating routes form the template via `TemplatedRouteBuilder` then you can provide the bean binding if you desire the bean to be local scoped (not shared with others):