You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2024/05/02 12:15:03 UTC

(camel) 08/09: CAMEL-20514: camel-model - Add support for bean constructors for beans in route templates or kamelets

This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch kamelet-ctr
in repository https://gitbox.apache.org/repos/asf/camel.git

commit bb4faac348eab61a30dea95f2158fad0d1ac2c63
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Thu May 2 14:11:13 2024 +0200

    CAMEL-20514: camel-model - Add support for bean constructors for beans in route templates or kamelets
---
 .../kamelet/KameletLocalBeanInitDestroyTest.java   | 86 ++++++++++++++++++++++
 .../src/main/java/org/apache/camel/Route.java      |  8 ++
 .../org/apache/camel/RouteTemplateContext.java     |  8 ++
 .../org/apache/camel/impl/engine/DefaultRoute.java | 15 ++++
 .../org/apache/camel/model/BeanModelHelper.java    | 42 +++++++++--
 .../camel/model/DefaultRouteTemplateContext.java   |  5 ++
 .../org/apache/camel/reifier/RouteReifier.java     | 26 +++++++
 .../apache/camel/support/LocalBeanRegistry.java    | 25 +++++++
 8 files changed, 209 insertions(+), 6 deletions(-)

diff --git a/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletLocalBeanInitDestroyTest.java b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletLocalBeanInitDestroyTest.java
new file mode 100644
index 00000000000..cd042bf1794
--- /dev/null
+++ b/components/camel-kamelet/src/test/java/org/apache/camel/component/kamelet/KameletLocalBeanInitDestroyTest.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.kamelet;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit5.CamelTestSupport;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class KameletLocalBeanInitDestroyTest extends CamelTestSupport {
+
+    private static final AtomicBoolean CLOSED = new AtomicBoolean();
+
+    @Test
+    public void testOne() throws Exception {
+        getMockEndpoint("mock:result").expectedBodiesReceived("Hi John we are going to Joes");
+
+        template.sendBody("direct:start", "John");
+
+        MockEndpoint.assertIsSatisfied(context);
+
+        context.stop();
+
+        Assertions.assertTrue(CLOSED.get(), "Should call destroy method on local bean");
+    }
+
+    // **********************************************
+    //
+    // test set-up
+    //
+    // **********************************************
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                routeTemplate("whereTo")
+                        .templateBean("myBar").typeClass(MyBar.class).initMethod("startupMe").destroyMethod("destroyMe").end()
+                        .from("kamelet:source")
+                        // must use {{myBar}} to refer to the local bean
+                        .to("bean:{{myBar}}");
+
+                from("direct:start")
+                        .kamelet("whereTo")
+                        .to("mock:result");
+            }
+        };
+    }
+
+    public static class MyBar {
+
+        private String bar;
+
+        public void startupMe() {
+            this.bar = "Joes";
+        }
+
+        public void destroyMe() {
+            this.bar = "closed";
+            CLOSED.set(true);
+        }
+
+        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/Route.java b/core/camel-api/src/main/java/org/apache/camel/Route.java
index d247d02240a..6c7f46c4c8b 100644
--- a/core/camel-api/src/main/java/org/apache/camel/Route.java
+++ b/core/camel-api/src/main/java/org/apache/camel/Route.java
@@ -211,6 +211,14 @@ public interface Route extends RuntimeConfiguration {
      */
     void addService(Service service);
 
+    /**
+     * Adds a service to this route
+     *
+     * @param service   the service
+     * @param forceStop whether to force stopping the service when the route stops
+     */
+    void addService(Service service, boolean forceStop);
+
     /**
      * Returns a navigator to navigate this route by navigating all the {@link Processor}s.
      *
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 855353d44b9..35cba9e23c8 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
@@ -92,6 +92,14 @@ public interface RouteTemplateContext extends HasCamelContext {
      */
     void bindAsPrototype(String id, Class<?> type, Supplier<Object> bean);
 
+    /**
+     * Registers an optional destroy method to invoke on the bean when stopping Camel.
+     *
+     * @param id     the id of the bean
+     * @param method the destroy method name
+     */
+    void registerDestroyMethod(String id, String method);
+
     /**
      * Gets the property with the given name
      *
diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRoute.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRoute.java
index be9e8836abb..003e83bef5b 100644
--- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRoute.java
+++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultRoute.java
@@ -107,6 +107,7 @@ public class DefaultRoute extends ServiceSupport implements Route {
     private final Endpoint endpoint;
     private final Map<String, Object> properties = new HashMap<>();
     private final List<Service> services = new ArrayList<>();
+    private final List<Service> servicesToStop = new ArrayList<>();
     private final StopWatch stopWatch = new StopWatch(false);
     private RouteError routeError;
     private Integer startupOrder;
@@ -236,6 +237,17 @@ public class DefaultRoute extends ServiceSupport implements Route {
         }
     }
 
+    @Override
+    public void addService(Service service, boolean forceStop) {
+        if (forceStop) {
+            if (!servicesToStop.contains(service)) {
+                servicesToStop.add(service);
+            }
+        } else {
+            addService(service);
+        }
+    }
+
     @Override
     public void warmUp() {
         // noop
@@ -274,6 +286,9 @@ public class DefaultRoute extends ServiceSupport implements Route {
     protected void doShutdown() throws Exception {
         // clear services when shutting down
         services.clear();
+        // shutdown forced services
+        ServiceHelper.stopAndShutdownService(servicesToStop);
+        servicesToStop.clear();
     }
 
     @Override
diff --git a/core/camel-core-model/src/main/java/org/apache/camel/model/BeanModelHelper.java b/core/camel-core-model/src/main/java/org/apache/camel/model/BeanModelHelper.java
index afdaddc6c9e..c949f1961a8 100644
--- a/core/camel-core-model/src/main/java/org/apache/camel/model/BeanModelHelper.java
+++ b/core/camel-core-model/src/main/java/org/apache/camel/model/BeanModelHelper.java
@@ -26,6 +26,7 @@ import org.apache.camel.Exchange;
 import org.apache.camel.Expression;
 import org.apache.camel.NoSuchBeanException;
 import org.apache.camel.RouteTemplateContext;
+import org.apache.camel.RuntimeCamelException;
 import org.apache.camel.model.app.RegistryBeanDefinition;
 import org.apache.camel.spi.ExchangeFactory;
 import org.apache.camel.spi.Language;
@@ -144,8 +145,6 @@ public final class BeanModelHelper {
         return target;
     }
 
-    // TODO: init and destroy method
-
     /**
      * Creates and binds the bean to the route-template repository (local beans for kamelets).
      *
@@ -191,12 +190,24 @@ public final class BeanModelHelper {
                 // and memorize so the script is only evaluated once and the local bean is the same
                 // if a route template refers to the local bean multiple times
                 routeTemplateContext.bind(def.getName(), clazz, Suppliers.memorize(() -> {
+                    Object local;
                     Map<String, Object> bindings = new HashMap<>();
                     // use rtx as the short-hand name, as context would imply its CamelContext
                     bindings.put("rtc", routeTemplateContext);
-                    Object local = slan.evaluate(script, bindings, Object.class);
-                    if (!props.isEmpty()) {
-                        PropertyBindingSupport.setPropertiesOnTarget(camelContext, local, props);
+                    try {
+                        local = slan.evaluate(script, bindings, Object.class);
+                        if (!props.isEmpty()) {
+                            PropertyBindingSupport.setPropertiesOnTarget(camelContext, local, props);
+                        }
+                        if (def.getInitMethod() != null) {
+                            org.apache.camel.support.ObjectHelper.invokeMethodSafe(def.getInitMethod(), local);
+                        }
+                        if (def.getDestroyMethod() != null) {
+                            routeTemplateContext.registerDestroyMethod(def.getName(), def.getDestroyMethod());
+                        }
+                    } catch (Exception e) {
+                        throw new IllegalStateException(
+                                "Cannot create bean: " + def.getType(), e);
                     }
                     return local;
                 }));
@@ -215,10 +226,23 @@ public final class BeanModelHelper {
                             if (!props.isEmpty()) {
                                 PropertyBindingSupport.setPropertiesOnTarget(camelContext, local, props);
                             }
+                            if (def.getInitMethod() != null) {
+                                try {
+                                    org.apache.camel.support.ObjectHelper.invokeMethodSafe(def.getInitMethod(), local);
+                                } catch (Exception e) {
+                                    throw RuntimeCamelException.wrapRuntimeException(e);
+                                }
+                            }
+                            if (def.getDestroyMethod() != null) {
+                                routeTemplateContext.registerDestroyMethod(def.getName(), def.getDestroyMethod());
+                            }
                             return local;
                         } else {
                             return null;
                         }
+                    } catch (Exception e) {
+                        throw new IllegalStateException(
+                                "Cannot create bean: " + def.getType(), e);
                     } finally {
                         ef.release(dummy);
                     }
@@ -267,10 +291,16 @@ public final class BeanModelHelper {
                             PropertyBindingSupport.setPropertiesOnTarget(camelContext, local, def.getProperties());
                         }
                     }
+                    if (def.getInitMethod() != null) {
+                        org.apache.camel.support.ObjectHelper.invokeMethodSafe(def.getInitMethod(), local);
+                    }
+                    if (def.getDestroyMethod() != null) {
+                        routeTemplateContext.registerDestroyMethod(def.getName(), def.getDestroyMethod());
+                    }
                     return local;
                 } catch (Exception e) {
                     throw new IllegalStateException(
-                            "Cannot create bean: " + def.getType());
+                            "Cannot create bean: " + def.getType(), e);
                 }
             }));
         } else {
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 07649447ae7..68f85c0395c 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
@@ -85,6 +85,11 @@ public final class DefaultRouteTemplateContext implements RouteTemplateContext {
         registry.bindAsPrototype(id, type, bean);
     }
 
+    @Override
+    public void registerDestroyMethod(String id, String method) {
+        registry.registerDestroyMethod(id, method);
+    }
+
     @Override
     public Object getProperty(String name) {
         return parameters.get(name);
diff --git a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/RouteReifier.java b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/RouteReifier.java
index a41d8cbf34c..614084798e9 100644
--- a/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/RouteReifier.java
+++ b/core/camel-core-reifier/src/main/java/org/apache/camel/reifier/RouteReifier.java
@@ -16,6 +16,8 @@
  */
 package org.apache.camel.reifier;
 
+import java.io.Closeable;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -31,6 +33,7 @@ import org.apache.camel.FailedToCreateRouteException;
 import org.apache.camel.Processor;
 import org.apache.camel.Route;
 import org.apache.camel.RuntimeCamelException;
+import org.apache.camel.Service;
 import org.apache.camel.ServiceStatus;
 import org.apache.camel.ShutdownRoute;
 import org.apache.camel.ShutdownRunningTask;
@@ -42,6 +45,7 @@ import org.apache.camel.model.RouteDefinition;
 import org.apache.camel.processor.ContractAdvice;
 import org.apache.camel.processor.RoutePipeline;
 import org.apache.camel.reifier.rest.RestBindingReifier;
+import org.apache.camel.spi.BeanRepository;
 import org.apache.camel.spi.CamelInternalProcessorAdvice;
 import org.apache.camel.spi.Contract;
 import org.apache.camel.spi.ErrorHandlerAware;
@@ -53,6 +57,8 @@ import org.apache.camel.spi.RoutePolicy;
 import org.apache.camel.spi.RoutePolicyFactory;
 import org.apache.camel.support.ExchangeHelper;
 import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.service.ServiceSupport;
+import org.apache.camel.util.IOHelper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -367,6 +373,26 @@ public class RouteReifier extends ProcessorReifier<RouteDefinition> {
             camelContext.getCamelContextExtension().addBootstrap(route::clearRouteModel);
         }
 
+        if (definition.getRouteTemplateContext() != null) {
+            // make route stop beans from the local repository (route templates / kamelets)
+            Service wrapper = new ServiceSupport() {
+                @Override
+                protected void doStop() throws Exception {
+                    close();
+                }
+
+                @Override
+                public void close() throws IOException {
+                    BeanRepository repo = definition.getRouteTemplateContext().getLocalBeanRepository();
+                    if (repo instanceof Closeable obj) {
+                        IOHelper.close(obj);
+                    }
+                    super.close();
+                }
+            };
+            route.addService(wrapper, true);
+        }
+
         return route;
     }
 
diff --git a/core/camel-support/src/main/java/org/apache/camel/support/LocalBeanRegistry.java b/core/camel-support/src/main/java/org/apache/camel/support/LocalBeanRegistry.java
index d43814d56c9..42d364542a5 100644
--- a/core/camel-support/src/main/java/org/apache/camel/support/LocalBeanRegistry.java
+++ b/core/camel-support/src/main/java/org/apache/camel/support/LocalBeanRegistry.java
@@ -16,7 +16,10 @@
  */
 package org.apache.camel.support;
 
+import java.io.IOException;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Set;
 
 /**
@@ -26,9 +29,15 @@ import java.util.Set;
  */
 public final class LocalBeanRegistry extends SupplierRegistry {
 
+    private final Map<String, String> destroyMethods = new HashMap<>();
+
     public LocalBeanRegistry() {
     }
 
+    public void registerDestroyMethod(String id, String method) {
+        destroyMethods.put(id, method);
+    }
+
     /**
      * Makes a copy of this registry
      */
@@ -42,4 +51,20 @@ public final class LocalBeanRegistry extends SupplierRegistry {
         return Collections.unmodifiableSet(keySet());
     }
 
+    @Override
+    public void close() throws IOException {
+        // stop all beans that has destroy method
+        destroyMethods.forEach((id, method) -> {
+            Object bean = lookupByName(id);
+            if (bean != null) {
+                try {
+                    org.apache.camel.support.ObjectHelper.invokeMethodSafe(method, bean);
+                } catch (Exception e) {
+                    // ignore
+                }
+            }
+        });
+        destroyMethods.clear();
+        super.close();
+    }
 }