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>