diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/ContextServiceLoaderPluginResolver.java b/core/camel-api/src/main/java/org/apache/camel/spi/ContextServiceLoaderPluginResolver.java index 4e49e02cc9205..0262342e1c9db 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/ContextServiceLoaderPluginResolver.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/ContextServiceLoaderPluginResolver.java @@ -38,4 +38,10 @@ * @see StatefulService */ public interface ContextServiceLoaderPluginResolver extends CamelContextAware, StatefulService { + + /** + * Invokes {@link ContextServicePlugin#onReload(org.apache.camel.CamelContext)} on all discovered plugins, giving + * them an opportunity to refresh their state before routes are reloaded. + */ + void onReload(); } diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/ContextServicePlugin.java b/core/camel-api/src/main/java/org/apache/camel/spi/ContextServicePlugin.java index 3dc07b7c2268e..d70be7d0adf03 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/ContextServicePlugin.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/ContextServicePlugin.java @@ -67,6 +67,22 @@ public interface ContextServicePlugin { */ void load(CamelContext camelContext); + /** + * Called before route reloading in development mode to allow the plugin to refresh its state. + *

+ * This method is invoked by the route watcher reload strategy before routes are reloaded, giving plugins the + * opportunity to refresh bean references, re-read configuration, or perform other updates that should happen before + * the new routes are started. + *

+ * The default implementation does nothing. Plugins that register beans or other resources that may become stale + * when properties change should override this method to refresh those resources. + * + * @param camelContext the CamelContext being reloaded, never {@code null} + */ + default void onReload(CamelContext camelContext) { + // NO-OP + } + /** * Called during CamelContext stop. Use it to free allocated resources. * diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultContextServiceLoaderPlugin.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultContextServiceLoaderPlugin.java index 8104a43bf2604..d964d9fa784c6 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultContextServiceLoaderPlugin.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultContextServiceLoaderPlugin.java @@ -76,6 +76,21 @@ protected void doStart() throws Exception { } } + @Override + public void onReload() { + if (contextServicePlugins != null) { + for (ContextServicePlugin plugin : contextServicePlugins) { + try { + plugin.onReload(camelContext); + } catch (Exception e) { + LOG.warn( + "Reloading of plugin {} failed, however the exception will be ignored so other plugins can be reloaded. Reason: {}", + plugin.getClass().getName(), e.getMessage(), e); + } + } + } + } + @Override protected void doStop() throws Exception { if (contextServicePlugins != null) { diff --git a/core/camel-core/src/test/java/org/apache/camel/support/RouteWatcherReloadStrategyOnReloadTest.java b/core/camel-core/src/test/java/org/apache/camel/support/RouteWatcherReloadStrategyOnReloadTest.java new file mode 100644 index 0000000000000..680e4a27b9b50 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/support/RouteWatcherReloadStrategyOnReloadTest.java @@ -0,0 +1,169 @@ +/* + * 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 java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.camel.CamelContext; +import org.apache.camel.ContextTestSupport; +import org.apache.camel.ServiceStatus; +import org.apache.camel.spi.ContextServiceLoaderPluginResolver; +import org.apache.camel.spi.ContextServicePlugin; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class RouteWatcherReloadStrategyOnReloadTest extends ContextTestSupport { + + @Test + public void testOnReloadInvokesPlugins() throws Exception { + AtomicInteger reloadCount = new AtomicInteger(); + + // Create a test plugin that tracks reload calls + ContextServicePlugin testPlugin = new ContextServicePlugin() { + @Override + public void load(CamelContext camelContext) { + // NO-OP + } + + @Override + public void onReload(CamelContext camelContext) { + reloadCount.incrementAndGet(); + } + }; + + // Register a custom resolver with our test plugin + ContextServiceLoaderPluginResolver resolver = new TestContextServiceLoaderPluginResolver(context, testPlugin); + context.getCamelContextExtension().addContextPlugin(ContextServiceLoaderPluginResolver.class, resolver); + + RouteWatcherReloadStrategy strategy = new RouteWatcherReloadStrategy(); + strategy.setCamelContext(context); + + // Trigger route reload + strategy.onRouteReload(Collections.emptyList(), false); + + assertEquals(1, reloadCount.get(), "onReload should have been called once on the plugin"); + + // Trigger another reload + strategy.onRouteReload(Collections.emptyList(), false); + + assertEquals(2, reloadCount.get(), "onReload should have been called twice on the plugin"); + } + + @Test + public void testOnReloadDefaultNoOp() throws Exception { + // Verify the default onReload is a no-op and does not throw + ContextServicePlugin plugin = new ContextServicePlugin() { + @Override + public void load(CamelContext camelContext) { + // NO-OP + } + }; + + // Should not throw + plugin.onReload(context); + } + + /** + * A simple test resolver that delegates to a single plugin. + */ + private static class TestContextServiceLoaderPluginResolver implements ContextServiceLoaderPluginResolver { + private CamelContext camelContext; + private final ContextServicePlugin plugin; + + TestContextServiceLoaderPluginResolver(CamelContext camelContext, ContextServicePlugin plugin) { + this.camelContext = camelContext; + this.plugin = plugin; + } + + @Override + public void onReload() { + plugin.onReload(camelContext); + } + + @Override + public void setCamelContext(CamelContext camelContext) { + this.camelContext = camelContext; + } + + @Override + public CamelContext getCamelContext() { + return camelContext; + } + + @Override + public void start() { + } + + @Override + public void stop() { + } + + @Override + public void suspend() { + } + + @Override + public void resume() { + } + + @Override + public void shutdown() { + } + + @Override + public ServiceStatus getStatus() { + return ServiceStatus.Started; + } + + @Override + public boolean isStarted() { + return true; + } + + @Override + public boolean isStarting() { + return false; + } + + @Override + public boolean isStopping() { + return false; + } + + @Override + public boolean isStopped() { + return false; + } + + @Override + public boolean isSuspending() { + return false; + } + + @Override + public boolean isSuspended() { + return false; + } + + @Override + public boolean isRunAllowed() { + return true; + } + } +} diff --git a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java index 77791c5895210..8a1d09885194d 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/RouteWatcherReloadStrategy.java @@ -32,6 +32,7 @@ import org.apache.camel.RuntimeCamelException; import org.apache.camel.ServiceStatus; import org.apache.camel.StartupSummaryLevel; +import org.apache.camel.spi.ContextServiceLoaderPluginResolver; import org.apache.camel.spi.GroovyScriptCompiler; import org.apache.camel.spi.PropertiesComponent; import org.apache.camel.spi.PropertiesReload; @@ -247,6 +248,13 @@ protected boolean onGroovyReload(Resource resource, boolean reloadRoutes) throws @SuppressWarnings("unchecked") protected void onRouteReload(Collection resources, boolean removeEverything) { + // notify context service plugins before reloading routes + ContextServiceLoaderPluginResolver pluginResolver + = getCamelContext().getCamelContextExtension().getContextPlugin(ContextServiceLoaderPluginResolver.class); + if (pluginResolver != null) { + pluginResolver.onReload(); + } + // remember all existing resources List sources = new ArrayList<>(); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java index d366bec583be9..3b9ff7c9cd622 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Run.java @@ -1000,8 +1000,18 @@ private int run() throws Exception { addRuntimeSpecificDependenciesFromProperties(profileProperties); // Add plugin dependencies + Map activePlugins = Collections.emptyMap(); if (!skipPlugins) { - Set exporters = PluginHelper.getActivePlugins(getMain(), repositories).values() + activePlugins = PluginHelper.getActivePlugins(getMain(), repositories); + + // Let plugins customize the run environment (e.g., set config directories) + // before plugin exporter dependencies are added, so exporters can scan the right locations + for (Plugin plugin : activePlugins.values()) { + plugin.getRunCustomizer() + .ifPresent(customizer -> customizer.beforeRun(main, Collections.unmodifiableList(files))); + } + + Set exporters = activePlugins.values() .stream() .map(Plugin::getExporter) .filter(Optional::isPresent) diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/Plugin.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/Plugin.java index 12db1df2eee9c..79e5f371973a9 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/Plugin.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/Plugin.java @@ -42,4 +42,15 @@ public interface Plugin { default Optional getExporter() { return Optional.empty(); } + + /** + * The plugin may provide an optional run customizer that is called after the Run command has resolved file + * arguments and basic dependencies, but before plugin exporter dependencies are added and KameletMain.run() builds + * the CamelContext. + * + * @return the plugin specific run customizer implementation, otherwise empty + */ + default Optional getRunCustomizer() { + return Optional.empty(); + } } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginRunCustomizer.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginRunCustomizer.java new file mode 100644 index 0000000000000..436cffd199f9b --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginRunCustomizer.java @@ -0,0 +1,40 @@ +/* + * 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.dsl.jbang.core.common; + +import java.util.List; + +import org.apache.camel.main.KameletMain; + +/** + * Plugin hook that runs after the Run command has resolved file arguments and basic dependencies, but before plugin + * exporter dependencies are added and {@link KameletMain#run()} builds the CamelContext. + * + * This allows plugins to customize the environment (system properties, config directories, initial properties) based on + * the file arguments passed to the run command, so that plugin exporters can scan the right locations. + */ +public interface PluginRunCustomizer { + + /** + * Called after the Run command has resolved file arguments and basic dependencies, but before plugin exporter + * dependencies are added and KameletMain.run() builds the CamelContext. + * + * @param main the KameletMain instance (for adding initial properties) + * @param files the resolved file arguments passed to the run command (read-only) + */ + void beforeRun(KameletMain main, List files); +} diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginRunCustomizerTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginRunCustomizerTest.java new file mode 100644 index 0000000000000..9fc851b7db94e --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginRunCustomizerTest.java @@ -0,0 +1,73 @@ +/* + * 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.dsl.jbang.core.common; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.main.KameletMain; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +import static org.junit.jupiter.api.Assertions.*; + +public class PluginRunCustomizerTest { + + @Test + public void testPluginDefaultReturnsEmpty() { + Plugin plugin = (commandLine, main) -> { + // no-op + }; + + assertTrue(plugin.getRunCustomizer().isEmpty()); + } + + @Test + public void testPluginWithRunCustomizer() { + List capturedFiles = new ArrayList<>(); + boolean[] called = { false }; + + PluginRunCustomizer customizer = (main, files) -> { + called[0] = true; + capturedFiles.addAll(files); + }; + + Plugin plugin = new Plugin() { + @Override + public void customize(CommandLine commandLine, CamelJBangMain main) { + // no-op + } + + @Override + public Optional getRunCustomizer() { + return Optional.of(customizer); + } + }; + + Optional result = plugin.getRunCustomizer(); + assertTrue(result.isPresent()); + + // Simulate invoking the customizer + List testFiles = List.of("route.yaml", "beans.xml"); + result.get().beforeRun(new KameletMain("test"), testFiles); + + assertTrue(called[0]); + assertEquals(testFiles, capturedFiles); + } +}