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 392d620d9a8f CAMEL-23835: camel-launcher - set classloader on embedded 
plugins so TUI ServiceLoader works in fat-jar
392d620d9a8f is described below

commit 392d620d9a8fccda2dc48aec958336cc0f2406f0
Author: Adriano Machado <[email protected]>
AuthorDate: Fri Jun 26 01:50:52 2026 -0400

    CAMEL-23835: camel-launcher - set classloader on embedded plugins so TUI 
ServiceLoader works in fat-jar
    
    The launcher registers embedded plugins in postAddCommands without setting a
    classloader. The TUI plugin installs it as the TCCL for tamboui's 
ServiceLoader
    backend discovery. With null classloader, ServiceLoader falls back to the 
system
    classloader which cannot see nested BOOT-INF/lib jars in the Spring Boot 
fat-jar,
    causing "No BackendProvider found on classpath".
    
    Fix: postAddCommands now sets the fat-jar classloader on each embedded 
plugin
    before calling customize(). Added regression test.
    
    Closes #24258
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 .../dsl/jbang/launcher/CamelLauncherMain.java      | 26 ++++++++++++----
 .../dsl/jbang/launcher/CamelLauncherTest.java      | 36 ++++++++++++++++++++++
 2 files changed, 56 insertions(+), 6 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherMain.java
 
b/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherMain.java
index 7c36cddb6b17..4df426be4e31 100644
--- 
a/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherMain.java
+++ 
b/dsl/camel-jbang/camel-launcher/src/main/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherMain.java
@@ -16,12 +16,15 @@
  */
 package org.apache.camel.dsl.jbang.launcher;
 
+import java.util.List;
+
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.generate.GeneratePlugin;
 import org.apache.camel.dsl.jbang.core.commands.kubernetes.KubernetesPlugin;
 import org.apache.camel.dsl.jbang.core.commands.test.TestPlugin;
 import org.apache.camel.dsl.jbang.core.commands.tui.TuiPlugin;
 import org.apache.camel.dsl.jbang.core.commands.validate.ValidatePlugin;
+import org.apache.camel.dsl.jbang.core.common.Plugin;
 import picocli.CommandLine;
 
 /**
@@ -31,12 +34,23 @@ public class CamelLauncherMain extends CamelJBangMain {
 
     @Override
     public void postAddCommands(CommandLine commandLine, String[] args) {
-        // install embedded plugins
-        new GeneratePlugin().customize(commandLine, this);
-        new KubernetesPlugin().customize(commandLine, this);
-        new TuiPlugin().customize(commandLine, this);
-        new ValidatePlugin().customize(commandLine, this);
-        new TestPlugin().customize(commandLine, this);
+        // Install embedded plugins. Each plugin must be given the classloader 
that loaded it (this
+        // class's classloader, i.e. the fat-jar classloader) so that plugins 
relying on ServiceLoader
+        // discovery can see the dependencies bundled in the launcher. The TUI 
plugin installs this
+        // classloader as the thread-context classloader so tamboui can 
discover its terminal backend;
+        // without it the discovery falls back to the system classloader, 
which cannot see the nested
+        // BOOT-INF/lib jars of the Spring Boot fat-jar, and the TUI fails 
with "No BackendProvider found".
+        ClassLoader classLoader = getClass().getClassLoader();
+        List<Plugin> plugins = List.of(
+                new GeneratePlugin(),
+                new KubernetesPlugin(),
+                new TuiPlugin(),
+                new ValidatePlugin(),
+                new TestPlugin());
+        for (Plugin plugin : plugins) {
+            plugin.setClassLoader(classLoader);
+            plugin.customize(commandLine, this);
+        }
     }
 
 }
diff --git 
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherTest.java
 
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherTest.java
index 8c5c52fe87b8..d09f76deb102 100644
--- 
a/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherTest.java
+++ 
b/dsl/camel-jbang/camel-launcher/src/test/java/org/apache/camel/dsl/jbang/launcher/CamelLauncherTest.java
@@ -18,12 +18,16 @@ package org.apache.camel.dsl.jbang.launcher;
 
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
+import java.lang.reflect.Field;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.camel.dsl.jbang.core.common.Printer;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import picocli.CommandLine;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -87,6 +91,38 @@ public class CamelLauncherTest {
         assertTrue(output.contains("Camel CLI version:"), "Output should 
contain version information");
     }
 
+    @Test
+    public void testEmbeddedTuiPluginReceivesClassLoader() throws Exception {
+        // Regression for the launcher TUI failure "No BackendProvider found 
on classpath".
+        // The launcher installs embedded plugins directly in postAddCommands. 
Each plugin must be
+        // given the fat-jar classloader so that the TUI can install it as the 
thread-context
+        // classloader for tamboui's ServiceLoader-based terminal backend 
discovery. Without it the
+        // discovery falls back to the system classloader, which cannot see 
the nested BOOT-INF/lib
+        // jars of the Spring Boot fat-jar.
+        CamelLauncherMain main = new CamelLauncherMain();
+
+        CommandLine commandLine = new CommandLine(main);
+        main.postAddCommands(commandLine, new String[] { "tui" });
+
+        CommandLine tui = commandLine.getSubcommands().get("tui");
+        assertNotNull(tui, "tui command should be registered by the launcher");
+        CommandLine monitor = tui.getSubcommands().get("monitor");
+        assertNotNull(monitor, "tui monitor subcommand should be registered");
+
+        ClassLoader pluginClassLoader = 
readClassLoaderField(monitor.getCommand());
+        assertNotNull(pluginClassLoader,
+                "embedded TUI plugin must receive a non-null classloader (else 
tamboui ServiceLoader breaks in the fat-jar)");
+        // The classloader handed to the plugin must be able to resolve 
tamboui's backend SPI.
+        assertDoesNotThrow(() -> 
pluginClassLoader.loadClass("dev.tamboui.terminal.BackendProvider"),
+                "the plugin classloader must be able to load tamboui's 
BackendProvider");
+    }
+
+    private static ClassLoader readClassLoaderField(Object command) throws 
Exception {
+        Field field = command.getClass().getDeclaredField("classLoader");
+        field.setAccessible(true);
+        return (ClassLoader) field.get(command);
+    }
+
     @Test
     public void testLauncherValidatePlugin() {
         ByteArrayOutputStream baos = new ByteArrayOutputStream();

Reply via email to