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

gnodet pushed a commit to branch camel-21658
in repository https://gitbox.apache.org/repos/asf/camel.git

commit adae349185224ed31bc37d329c6bae732362ed48
Author: Guillaume Nodet <gno...@gnodet-mac.chimera-diatonic.ts.net>
AuthorDate: Tue May 27 02:03:15 2025 +0200

    Support for loading plugins from classpath
---
 .../camel/dsl/jbang/core/common/PluginHelper.java  | 175 ++++++++++++++++++++-
 .../dsl/jbang/core/common/PluginHelperTest.java    |  77 +++++++++
 dsl/camel-jbang/camel-jbang-launcher/pom.xml       |  20 +++
 3 files changed, 268 insertions(+), 4 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
index 0f00bc887bf..7501259c58a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginHelper.java
@@ -18,12 +18,16 @@ package org.apache.camel.dsl.jbang.core.common;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
+import java.util.Enumeration;
 import java.util.Optional;
 import java.util.Properties;
 import java.util.function.Supplier;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
 
 import org.apache.camel.RuntimeCamelException;
 import org.apache.camel.catalog.CamelCatalog;
@@ -49,6 +53,7 @@ import picocli.CommandLine;
 public final class PluginHelper {
 
     public static final String PLUGIN_CONFIG = ".camel-jbang-plugins.json";
+    public static final String PLUGIN_SERVICE_DIR = 
"META-INF/services/org/apache/camel/camel-jbang-plugin/";
 
     private static final FactoryFinder FACTORY_FINDER
             = new DefaultFactoryFinder(new DefaultClassResolver(), 
FactoryFinder.DEFAULT_PATH + "camel-jbang-plugin/");
@@ -66,11 +71,28 @@ public final class PluginHelper {
      * @param main        the current Camel JBang main
      */
     public static void addPlugins(CommandLine commandLine, CamelJBangMain 
main, String... args) {
-        JsonObject config = getPluginConfig();
-
         // first arg is the command name (ie camel generate xxx)
         String target = args != null && args.length > 0 ? args[0] : null;
 
+        // First, try to load embedded plugins from classpath (fat-jar 
scenario)
+        boolean foundEmbeddedPlugins = false;
+        try {
+            foundEmbeddedPlugins = addEmbeddedPlugins(commandLine, main, 
target);
+        } catch (Exception e) {
+            // Ignore errors in embedded plugin loading
+        }
+
+        // If we found embedded plugins and we're looking for a specific 
target,
+        // check if it was satisfied by embedded plugins
+        if (foundEmbeddedPlugins && target != null && !"shell".equals(target)) 
{
+            // Check if the target command was added by embedded plugins
+            if (commandLine.getSubcommands().containsKey(target)) {
+                return; // Target satisfied by embedded plugins, no need for 
JSON config
+            }
+        }
+
+        // Fall back to JSON configuration for additional or missing plugins
+        JsonObject config = getPluginConfig();
         if (config != null) {
             CamelCatalog catalog = new DefaultCamelCatalog();
             String version = catalog.getCatalogVersion();
@@ -88,6 +110,11 @@ public final class PluginHelper {
                     continue;
                 }
 
+                // Skip if this plugin was already loaded from embedded plugins
+                if (foundEmbeddedPlugins && 
commandLine.getSubcommands().containsKey(command)) {
+                    continue;
+                }
+
                 // check if plugin version can be loaded (cannot if we use an 
older camel version than the plugin)
                 if (!version.isBlank() && !firstVersion.isBlank()) {
                     versionCheck(main, version, firstVersion, command);
@@ -120,7 +147,7 @@ public final class PluginHelper {
         return MavenGav.parseGav(dependency.toString());
     }
 
-    private static void versionCheck(CamelJBangMain main, String version, 
String firstVersion, String command) {
+    static void versionCheck(CamelJBangMain main, String version, String 
firstVersion, String command) {
         // compare versions without SNAPSHOT
         String source = version;
         if (source.endsWith("-SNAPSHOT")) {
@@ -172,7 +199,7 @@ public final class PluginHelper {
         return 
Optional.ofNullable(getPluginConfig()).orElseGet(PluginHelper::createPluginConfig);
     }
 
-    private static JsonObject getPluginConfig() {
+    static JsonObject getPluginConfig() {
         try {
             Path f = CommandLineHelper.getHomeDir().resolve(PLUGIN_CONFIG);
             if (Files.exists(f)) {
@@ -266,4 +293,144 @@ public final class PluginHelper {
     private static String extractVersion(MavenGav gav, String defaultVersion) {
         return doExtractInfo(gav, defaultVersion, gav != null ? 
gav::getVersion : () -> "");
     }
+
+    /**
+     * Scans the classpath for embedded plugins and loads them directly. This 
bypasses the JSON configuration and
+     * download phase for fat-jar scenarios.
+     */
+    public static boolean addEmbeddedPlugins(CommandLine commandLine, 
CamelJBangMain main, String target) {
+        boolean foundAny = false;
+        try {
+            ClassLoader classLoader = PluginHelper.class.getClassLoader();
+            String serviceDir = PLUGIN_SERVICE_DIR;
+
+            // If we didn't find individual services, try scanning jar
+            if (!foundAny) {
+                Enumeration<URL> resources = 
classLoader.getResources(serviceDir);
+                while (resources.hasMoreElements()) {
+                    URL url = resources.nextElement();
+                    if (url.getProtocol().equals("jar")) {
+                        foundAny = scanJarForPlugins(commandLine, main, 
target, classLoader, url);
+                        break; // Found jar, no need to continue
+                    }
+                }
+            }
+        } catch (Exception e) {
+            // Ignore errors in classpath scanning
+        }
+        return foundAny;
+    }
+
+    private static boolean scanJarForPlugins(
+            CommandLine commandLine, CamelJBangMain main, String target,
+            ClassLoader classLoader, URL jarUrl) {
+        boolean foundAny = false;
+        try {
+            String jarPath = jarUrl.getPath();
+            if (jarPath.startsWith("file:")) {
+                jarPath = jarPath.substring(5);
+            }
+            if (jarPath.contains("!")) {
+                jarPath = jarPath.substring(0, jarPath.indexOf("!"));
+            }
+
+            try (JarFile jarFile = new JarFile(jarPath)) {
+                Enumeration<JarEntry> entries = jarFile.entries();
+                while (entries.hasMoreElements()) {
+                    JarEntry entry = entries.nextElement();
+                    String entryName = entry.getName();
+                    if (entryName.startsWith(PLUGIN_SERVICE_DIR)
+                            && !entryName.endsWith("/")) {
+                        String pluginName = 
entryName.substring(entryName.lastIndexOf("/") + 1);
+                        URL serviceUrl = classLoader.getResource(entryName);
+                        if (serviceUrl != null) {
+                            if (loadPluginFromService(commandLine, main, 
target, classLoader, serviceUrl, pluginName)) {
+                                foundAny = true;
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            // Ignore errors
+        }
+        return foundAny;
+    }
+
+    private static boolean loadPluginFromService(
+            CommandLine commandLine, CamelJBangMain main, String target,
+            ClassLoader classLoader, URL serviceUrl, String pluginName) {
+        try (InputStream is = serviceUrl.openStream()) {
+            Properties prop = new Properties();
+            prop.load(is);
+            String pluginClassName = prop.getProperty("class");
+            if (pluginClassName != null) {
+                Class<?> pluginClass = classLoader.loadClass(pluginClassName);
+                Plugin plugin = (Plugin) 
pluginClass.getDeclaredConstructor().newInstance();
+
+                // Extract command name from plugin name
+                String command = extractCommandFromPlugin(pluginClass, 
pluginName);
+
+                // Only load the plugin if the command-line is calling this 
plugin or if target is null (shell mode)
+                if (target != null && !"shell".equals(target) && 
!target.equals(command)) {
+                    return false;
+                }
+
+                // Check version compatibility if needed
+                CamelJBangPlugin annotation = 
pluginClass.getAnnotation(CamelJBangPlugin.class);
+                if (annotation != null) {
+                    CamelCatalog catalog = new DefaultCamelCatalog();
+                    String version = catalog.getCatalogVersion();
+                    String firstVersion = annotation.firstVersion();
+                    if (!version.isBlank() && !firstVersion.isBlank()) {
+                        PluginHelper.versionCheck(main, version, firstVersion, 
command);
+                    }
+                }
+
+                plugin.customize(commandLine, main);
+                return true;
+            }
+        } catch (Exception e) {
+            // Ignore individual plugin loading errors
+        }
+        return false;
+    }
+
+    private static String extractCommandFromPlugin(Class<?> pluginClass, 
String pluginName) {
+        // Try to extract command from plugin name
+        if (pluginName.startsWith("camel-jbang-plugin-")) {
+            return pluginName.substring("camel-jbang-plugin-".length());
+        }
+
+        // Fallback to class name analysis
+        String className = pluginClass.getSimpleName();
+        if (className.endsWith("Plugin")) {
+            String command = className.substring(0, className.length() - 
6).toLowerCase();
+            return command;
+        }
+
+        return pluginName;
+    }
+
+    /**
+     * Checks if embedded plugins are available in the classpath.
+     */
+    public static boolean hasEmbeddedPlugins() {
+        try {
+            ClassLoader classLoader = PluginHelper.class.getClassLoader();
+            String serviceDir = PLUGIN_SERVICE_DIR;
+
+            // Check if we're in a jar with plugin services
+            Enumeration<URL> resources = classLoader.getResources(serviceDir);
+            while (resources.hasMoreElements()) {
+                URL url = resources.nextElement();
+                if (url.getProtocol().equals("jar")) {
+                    return true;
+                }
+            }
+        } catch (Exception e) {
+            // Ignore errors
+        }
+        return false;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginHelperTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginHelperTest.java
new file mode 100644
index 00000000000..34c3e7daaf4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginHelperTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class PluginHelperTest {
+
+    @TempDir
+    Path tempDir;
+
+    @BeforeEach
+    public void setup() {
+        CommandLineHelper.useHomeDir(tempDir.toString());
+    }
+
+    @Test
+    public void testEmbeddedPluginDetection() throws Exception {
+        // Test that embedded plugins can be detected
+        boolean hasEmbedded = PluginHelper.hasEmbeddedPlugins();
+        // This may be false in test environment, but should not throw 
exceptions
+        assertNotNull(hasEmbedded);
+    }
+
+    @Test
+    public void testFallbackToJsonConfig() throws Exception {
+        // Create a user plugin config file in home directory
+        Path userConfig = 
CommandLineHelper.getHomeDir().resolve(PluginHelper.PLUGIN_CONFIG);
+        String userConfigContent = """
+                {
+                  "plugins": {
+                    "user-plugin": {
+                      "name": "user-plugin",
+                      "command": "user",
+                      "description": "User plugin",
+                      "firstVersion": "2.0.0"
+                    }
+                  }
+                }
+                """;
+        Files.writeString(userConfig, userConfigContent, 
StandardOpenOption.CREATE);
+
+        // Test that user config is loaded
+        JsonObject config = PluginHelper.getPluginConfig();
+        assertNotNull(config);
+
+        JsonObject plugins = config.getMap("plugins");
+        assertNotNull(plugins);
+
+        // Should have user plugin
+        JsonObject userPlugin = plugins.getMap("user-plugin");
+        assertNotNull(userPlugin);
+    }
+}
diff --git a/dsl/camel-jbang/camel-jbang-launcher/pom.xml 
b/dsl/camel-jbang/camel-jbang-launcher/pom.xml
index 54003739bda..4d6ab48db3a 100644
--- a/dsl/camel-jbang/camel-jbang-launcher/pom.xml
+++ b/dsl/camel-jbang/camel-jbang-launcher/pom.xml
@@ -63,6 +63,23 @@
             <artifactId>camel-jbang-core</artifactId>
             <version>${project.version}</version>
         </dependency>
+        
+        <!-- Pre-installed plugins -->
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-jbang-plugin-edit</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-jbang-plugin-generate</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-jbang-plugin-kubernetes</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.camel.kamelets</groupId>
             <artifactId>camel-kamelets</artifactId>
@@ -253,6 +270,9 @@
                                 <transformer 
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                     
<resource>META-INF/services/org/apache/camel/language.properties</resource>
                                 </transformer>
+                                <transformer 
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
+                                    
<resource>META-INF/services/org/apache/camel/camel-jbang-plugin</resource>
+                                </transformer>
                             </transformers>
                             <filters>
                                 <filter>

Reply via email to