diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc index d95a9d8160a26..02972e2360efc 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-jbang.adoc @@ -1251,11 +1251,20 @@ camel plugin get --all ---- Supported plugins: - NAME COMMAND DEPENDENCY DESCRIPTION - kubernetes kubernetes org.apache.camel:camel-jbang-plugin-kubernetes Run Camel applications on Kubernetes - generate generate org.apache.camel:camel-jbang-plugin-generate Generate code such as DTOs + NAME COMMAND VENDOR DEPENDENCY DESCRIPTION + kubernetes kubernetes ASF org.apache.camel:camel-jbang-plugin-kubernetes Run Camel applications on Kubernetes + generate generate ASF org.apache.camel:camel-jbang-plugin-generate Generate code such as DTOs + ... + +Known 3rd party plugins: + + NAME COMMAND VENDOR DEPENDENCY DESCRIPTION + forage forage Community io.kaoto.forage:camel-jbang-plugin-forage Utilities for working with Forage components + camel-kit kit Community io.github.luigidemasi:camel-kit-jbang-plugin Design Apache Camel Integrations with AI ---- +The `VENDOR` column indicates whether the plugin is from `ASF` (Apache Software Foundation) or `Community` (3rd party). + In case you want to enable a plugin and its functionality, you can add it as follows: [source,bash] @@ -1272,6 +1281,20 @@ camel plugin add generate This adds the plugin, and all subcommands are now available for execution. +Known 3rd party plugins can be installed by name as well. For example: + +[source,bash] +---- +camel plugin add forage +---- + +To install a specific version of a 3rd party plugin, use the `--version` option: + +[source,bash] +---- +camel plugin add forage --version=1.2.3 +---- + You can list the currently installed plugins with: [source,bash] diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAdd.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAdd.java index a1ad1998f9ad9..82c1c0b15de89 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAdd.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginAdd.java @@ -21,6 +21,7 @@ import org.apache.camel.catalog.CamelCatalog; import org.apache.camel.catalog.DefaultCamelCatalog; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.dsl.jbang.core.common.PluginHelper; import org.apache.camel.dsl.jbang.core.common.PluginType; import org.apache.camel.util.json.JsonObject; import picocli.CommandLine; @@ -89,6 +90,30 @@ public Integer doCall() throws Exception { if (firstVersion == null) { firstVersion = camelPlugin.get().getFirstVersion(); } + } else { + Optional known = PluginHelper.findKnownPlugin(name); + if (known.isPresent()) { + JsonObject kp = known.get(); + if (command == null) { + command = kp.getString("command"); + } + if (description == null) { + description = kp.getString("description"); + } + if (firstVersion == null) { + firstVersion = kp.getString("firstVersion"); + } + if (gav == null) { + groupId = kp.getStringOrDefault("groupId", groupId); + artifactId = kp.getString("artifactId"); + } + if (repositories == null) { + String knownRepos = kp.getString("repos"); + if (knownRepos != null) { + repositories = knownRepos; + } + } + } } if (command == null) { @@ -111,8 +136,12 @@ public Integer doCall() throws Exception { if (gav == null && (groupId != null && artifactId != null)) { if (version == null) { - CamelCatalog catalog = new DefaultCamelCatalog(); - version = catalog.getCatalogVersion(); + if ("org.apache.camel".equals(groupId)) { + CamelCatalog catalog = new DefaultCamelCatalog(); + version = catalog.getCatalogVersion(); + } else { + version = "LATEST"; + } } gav = "%s:%s:%s".formatted(groupId, artifactId, version); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginGet.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginGet.java index 9e17ae314ec7c..0395a5f5f7209 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginGet.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginGet.java @@ -25,6 +25,7 @@ import com.github.freva.asciitable.HorizontalAlign; import com.github.freva.asciitable.OverflowBehaviour; import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain; +import org.apache.camel.dsl.jbang.core.common.PluginHelper; import org.apache.camel.dsl.jbang.core.common.PluginType; import org.apache.camel.util.json.JsonObject; import picocli.CommandLine; @@ -59,7 +60,8 @@ public Integer doCall() throws Exception { = details.getStringOrDefault("description", "Plugin %s called with command %s".formatted(name, command)); String repos = details.getString("repos"); - rows.add(new Row(name, command, dependency, description, repos)); + String vendor = resolveVendor(name); + rows.add(new Row(name, command, dependency, description, repos, vendor)); }); printRows(rows); @@ -71,7 +73,7 @@ public Integer doCall() throws Exception { String dependency = "org.apache.camel:camel-jbang-plugin-%s".formatted(camelPlugin.getCommand()); rows.add(new Row( camelPlugin.getName(), camelPlugin.getCommand(), dependency, - camelPlugin.getDescription(), camelPlugin.getRepos())); + camelPlugin.getDescription(), camelPlugin.getRepos(), camelPlugin.getVendor())); } } @@ -82,11 +84,41 @@ public Integer doCall() throws Exception { printRows(rows); } + + rows.clear(); + List knownPlugins = PluginHelper.loadKnownPlugins(); + for (JsonObject kp : knownPlugins) { + String kpName = kp.getString("name"); + if (plugins.get(kpName) == null && PluginType.findByName(kpName).isEmpty()) { + String dep = kp.getString("groupId") != null && kp.getString("artifactId") != null + ? "%s:%s".formatted(kp.getString("groupId"), kp.getString("artifactId")) + : kp.getStringOrDefault("dependency", ""); + rows.add(new Row( + kpName, kp.getString("command"), dep, + kp.getString("description"), kp.getString("repos"), + kp.getStringOrDefault("vendor", "Community"))); + } + } + + if (!rows.isEmpty()) { + printer().println(); + printer().println("Known 3rd party plugins:"); + printer().println(); + + printRows(rows); + } } return 0; } + private String resolveVendor(String name) { + return PluginType.findByName(name) + .map(PluginType::getVendor) + .or(() -> PluginHelper.findKnownPlugin(name).map(kp -> kp.getStringOrDefault("vendor", "Community"))) + .orElse(""); + } + private void printRows(List rows) { if (!rows.isEmpty()) { printer().println(AsciiTable.getTable(AsciiTable.NO_BORDERS, rows, Arrays.asList( @@ -94,6 +126,8 @@ private void printRows(List rows) { .with(r -> r.name), new Column().header("COMMAND").headerAlign(HorizontalAlign.LEFT).dataAlign(HorizontalAlign.LEFT) .with(r -> r.command), + new Column().header("VENDOR").headerAlign(HorizontalAlign.LEFT).dataAlign(HorizontalAlign.LEFT) + .with(r -> r.vendor != null ? r.vendor : ""), new Column().header("DEPENDENCY").headerAlign(HorizontalAlign.LEFT).dataAlign(HorizontalAlign.LEFT) .with(r -> r.dependency), new Column().visible(repos).header("REPOSITORY").headerAlign(HorizontalAlign.LEFT) @@ -105,6 +139,6 @@ private void printRows(List rows) { } } - private record Row(String name, String command, String dependency, String description, String repos) { + private record Row(String name, String command, String dependency, String description, String repos, String vendor) { } } 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 36b01f5e3edab..0fa2eb0a1f553 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 @@ -20,6 +20,7 @@ import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; @@ -50,6 +51,7 @@ import org.apache.camel.support.ObjectHelper; import org.apache.camel.tooling.maven.MavenGav; import org.apache.camel.util.IOHelper; +import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; import org.apache.camel.util.json.Jsoner; import picocli.CommandLine; @@ -575,6 +577,37 @@ public static void savePluginConfig(JsonObject plugins) { } } + /** + * Loads the list of known 3rd party plugins from the classpath resource known-plugins.json. + */ + public static List loadKnownPlugins() { + try (InputStream is = PluginHelper.class.getClassLoader().getResourceAsStream("known-plugins.json")) { + if (is != null) { + String text = new String(is.readAllBytes(), StandardCharsets.UTF_8); + JsonArray arr = (JsonArray) Jsoner.deserialize(text); + List result = new ArrayList<>(arr.size()); + for (Object o : arr) { + if (o instanceof JsonObject jo) { + result.add(jo); + } + } + return result; + } + } catch (Exception e) { + // ignore + } + return List.of(); + } + + /** + * Finds a known 3rd party plugin by name (case-insensitive). + */ + public static Optional findKnownPlugin(String name) { + return loadKnownPlugins().stream() + .filter(p -> name.equalsIgnoreCase(p.getString("name"))) + .findFirst(); + } + public static void enable(PluginType pluginType) { JsonObject pluginConfig = PluginHelper.getOrCreatePluginConfig(); JsonObject plugins = pluginConfig.getMap("plugins"); diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginType.java b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginType.java index 0e3eb1d5df4c9..32ab80632d444 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginType.java +++ b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/PluginType.java @@ -24,26 +24,28 @@ */ public enum PluginType { - KUBERNETES("kubernetes", "kubernetes", "Run Camel applications on Kubernetes", "4.8.0", null), - GENERATE("generate", "generate", "Generate code such as DTOs", "4.8.0", null), - EDIT("edit", "edit", "Edit Camel files with suggestions", "4.12.0", null), - TEST("test", "test", "Manage tests for Camel applications", "4.14.0", null), - ROUTE_PARSER("route-parser", "route-parser", "Parses Java route and dumps route structure", "4.17.0", null), - VALIDATE("validate", "validate", "Validate Camel routes", "4.18.0", null), - TUI("tui", "tui", "Camel Dashboard", "4.20.0", null); + KUBERNETES("kubernetes", "kubernetes", "Run Camel applications on Kubernetes", "4.8.0", null, "ASF"), + GENERATE("generate", "generate", "Generate code such as DTOs", "4.8.0", null, "ASF"), + EDIT("edit", "edit", "Edit Camel files with suggestions", "4.12.0", null, "ASF"), + TEST("test", "test", "Manage tests for Camel applications", "4.14.0", null, "ASF"), + ROUTE_PARSER("route-parser", "route-parser", "Parses Java route and dumps route structure", "4.17.0", null, "ASF"), + VALIDATE("validate", "validate", "Validate Camel routes", "4.18.0", null, "ASF"), + TUI("tui", "tui", "Camel Dashboard", "4.20.0", null, "ASF"); private final String name; private final String command; private final String description; private final String firstVersion; private final String repos; + private final String vendor; - PluginType(String name, String command, String description, String firstVersion, String repos) { + PluginType(String name, String command, String description, String firstVersion, String repos, String vendor) { this.name = name; this.command = command; this.description = description; this.firstVersion = firstVersion; this.repos = repos; + this.vendor = vendor; } public static Optional findByName(String name) { @@ -71,4 +73,8 @@ public String getFirstVersion() { public String getRepos() { return repos; } + + public String getVendor() { + return vendor; + } } diff --git a/dsl/camel-jbang/camel-jbang-core/src/main/resources/known-plugins.json b/dsl/camel-jbang/camel-jbang-core/src/main/resources/known-plugins.json new file mode 100644 index 0000000000000..4ed20907a0a41 --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-core/src/main/resources/known-plugins.json @@ -0,0 +1,20 @@ +[ + { + "name": "forage", + "command": "forage", + "description": "Configure components via opinionated bean factories", + "firstVersion": "4.10.0", + "groupId": "io.kaoto.forage", + "artifactId": "camel-jbang-plugin-forage", + "vendor": "Community" + }, + { + "name": "camel-kit", + "command": "kit", + "description": "Design Apache Camel Integrations with AI", + "firstVersion": "4.10.0", + "groupId": "io.github.luigidemasi", + "artifactId": "camel-kit-jbang-plugin", + "vendor": "Community" + } +] diff --git a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginGetTest.java b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginGetTest.java index 9fef8e54f0bb5..ba950afb31bb4 100644 --- a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginGetTest.java +++ b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/plugin/PluginGetTest.java @@ -55,10 +55,10 @@ public void shouldGetPlugin() throws Exception { List output = printer.getLines(); Assertions.assertEquals(2, output.size()); - Assertions.assertEquals("NAME COMMAND DEPENDENCY DESCRIPTION", + Assertions.assertEquals("NAME COMMAND VENDOR DEPENDENCY DESCRIPTION", output.get(0)); Assertions.assertEquals( - "kubernetes kubernetes org.apache.camel:camel-jbang-plugin-kubernetes %s" + "kubernetes kubernetes ASF org.apache.camel:camel-jbang-plugin-kubernetes %s" .formatted(PluginType.KUBERNETES.getDescription()), output.get(1)); } @@ -70,12 +70,13 @@ public void shouldGetDefaultPlugins() throws Exception { command.doCall(); List output = printer.getLines(); - Assertions.assertEquals(10, output.size()); + Assertions.assertTrue(output.size() >= 10); Assertions.assertEquals("Supported plugins:", output.get(0)); - Assertions.assertEquals("NAME COMMAND DEPENDENCY DESCRIPTION", + Assertions.assertEquals( + "NAME COMMAND VENDOR DEPENDENCY DESCRIPTION", output.get(2)); Assertions.assertEquals( - "kubernetes kubernetes org.apache.camel:camel-jbang-plugin-kubernetes %s" + "kubernetes kubernetes ASF org.apache.camel:camel-jbang-plugin-kubernetes %s" .formatted(PluginType.KUBERNETES.getDescription()), output.get(3)); } @@ -97,8 +98,9 @@ public void shouldGenerateDependencyAndDescription() throws Exception { List output = printer.getLines(); Assertions.assertEquals(2, output.size()); - Assertions.assertEquals("NAME COMMAND DEPENDENCY DESCRIPTION", output.get(0)); - Assertions.assertEquals("foo foo org.apache.camel:camel-jbang-plugin-foo Plugin foo called with command foo", + Assertions.assertEquals("NAME COMMAND VENDOR DEPENDENCY DESCRIPTION", output.get(0)); + Assertions.assertEquals( + "foo foo org.apache.camel:camel-jbang-plugin-foo Plugin foo called with command foo", output.get(1)); } @@ -120,43 +122,46 @@ public void shouldGetAllPlugins() throws Exception { command.doCall(); List output = printer.getLines(); - Assertions.assertEquals(13, output.size()); - Assertions.assertEquals("NAME COMMAND DEPENDENCY DESCRIPTION", output.get(0)); + Assertions.assertTrue(output.size() >= 13); + Assertions.assertEquals("NAME COMMAND VENDOR DEPENDENCY DESCRIPTION", output.get(0)); Assertions.assertEquals( - "foo-plugin foo org.apache.camel:foo-plugin:1.0.0 Plugin foo-plugin called with command foo", + "foo-plugin foo org.apache.camel:foo-plugin:1.0.0 Plugin foo-plugin called with command foo", output.get(1)); Assertions.assertEquals("Supported plugins:", output.get(3)); - Assertions.assertEquals("NAME COMMAND DEPENDENCY DESCRIPTION", + Assertions.assertEquals( + "NAME COMMAND VENDOR DEPENDENCY DESCRIPTION", output.get(5)); Assertions.assertEquals( - "kubernetes kubernetes org.apache.camel:camel-jbang-plugin-kubernetes %s" + "kubernetes kubernetes ASF org.apache.camel:camel-jbang-plugin-kubernetes %s" .formatted(PluginType.KUBERNETES.getDescription()), output.get(6)); Assertions.assertEquals( - "generate generate org.apache.camel:camel-jbang-plugin-generate %s" + "generate generate ASF org.apache.camel:camel-jbang-plugin-generate %s" .formatted(PluginType.GENERATE.getDescription()), output.get(7)); Assertions.assertEquals( - "edit edit org.apache.camel:camel-jbang-plugin-edit %s" + "edit edit ASF org.apache.camel:camel-jbang-plugin-edit %s" .formatted(PluginType.EDIT.getDescription()), output.get(8)); Assertions.assertEquals( - "test test org.apache.camel:camel-jbang-plugin-test %s" + "test test ASF org.apache.camel:camel-jbang-plugin-test %s" .formatted(PluginType.TEST.getDescription()), output.get(9)); Assertions.assertEquals( - "route-parser route-parser org.apache.camel:camel-jbang-plugin-route-parser %s" + "route-parser route-parser ASF org.apache.camel:camel-jbang-plugin-route-parser %s" .formatted(PluginType.ROUTE_PARSER.getDescription()), output.get(10)); Assertions.assertEquals( - "validate validate org.apache.camel:camel-jbang-plugin-validate %s" + "validate validate ASF org.apache.camel:camel-jbang-plugin-validate %s" .formatted(PluginType.VALIDATE.getDescription()), output.get(11)); Assertions.assertEquals( - "tui tui org.apache.camel:camel-jbang-plugin-tui %s" + "tui tui ASF org.apache.camel:camel-jbang-plugin-tui %s" .formatted(PluginType.TUI.getDescription()), output.get(12)); + + Assertions.assertEquals("Known 3rd party plugins:", output.get(14)); } }