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

christophd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git

commit c9f339ed8091c96f0e60d4b9309ea5b3f1bc1988
Author: Christoph Deppisch <[email protected]>
AuthorDate: Wed Nov 11 23:07:34 2026 +0100

    CAMEL-26764: Backward-compatible appliesTo filtering, docs, test 
improvements
    
    - Missing appliesTo in config is treated as legacy (load for all
      built-in commands like run/export) to avoid breaking third-party
      plugins that haven't declared appliesTo yet.
    - When appliesTo is explicitly present (even empty []), filtering
      applies — only declared commands load the plugin.
    - Known PluginType appliesTo is merged at runtime without writing
      to the config file or emitting warnings.
    - After a plugin is downloaded, appliesTo is learned from its
      @CamelJBangPlugin annotation and cached in the resolved block.
    - Added upgrade guide entry documenting the new appliesTo attribute.
    - Tests use proper import for DefaultCamelCatalog, broken entries
      prove plugin inclusion, and legacy backward-compat is tested.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  14 +++
 .../camel/dsl/jbang/core/common/PluginHelper.java  |  57 ++++++---
 .../dsl/jbang/core/common/PluginFilteringTest.java | 138 ++++++++++++---------
 3 files changed, 133 insertions(+), 76 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index d9bf677e1888..4c554e0fe7fb 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -1178,6 +1178,20 @@ built for Camel 4.24:
 * A test sets the options of a command with 
`CommandLineTestHelper.parse(command, args...)` (in the test jar of
   `camel-jbang-core`) instead of `CommandLine.populateCommand`.
 
+=== camel-jbang - plugins declare which commands they contribute to
+
+A CLI plugin can now declare which built-in commands it participates in beyond 
its own command, using the
+`appliesTo` attribute on `@CamelJBangPlugin`. For example, a plugin that 
provides a `PluginExporter` should declare
+`@CamelJBangPlugin(name = "...", firstVersion = "...", appliesTo = 
{"export"})` so it is loaded during `camel export`
+but not during `camel run`.
+
+Existing plugins that do not declare `appliesTo` are loaded for all built-in 
commands as before (backward compatible).
+Once a plugin declares `appliesTo` (even an empty array), only the declared 
commands load it.
+
+When adding a plugin via `camel plugin add`, the `appliesTo` value from the 
`@CamelJBangPlugin` annotation (for known
+Camel plugins) or from the plugin's metadata is stored in the configuration. 
After a plugin is downloaded for the first
+time, its `appliesTo` is learned from its annotation and cached for subsequent 
invocations.
+
 === camel-resilience4j, camel-microprofile-fault-tolerance - a circuit breaker 
in a doTry
 
 A circuit breaker in the steps of a `doTry` no longer ends that `doTry` for 
the steps after it. Before, when the
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 a6ab03f9dd98..9fb61f22bc64 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
@@ -361,23 +361,14 @@ public final class PluginHelper {
                 final String firstVersion = 
properties.getOrDefault("firstVersion", "").toString();
                 final String gav = properties.getOrDefault("dependency", 
"").toString();
 
-                // auto-migrate old config entries for known plugins that now 
declare appliesTo
-                if (properties.get("appliesTo") == null) {
-                    Optional<PluginType> knownType = 
PluginType.findByName(name);
-                    if (knownType.isPresent() && 
knownType.get().getAppliesTo().length > 0) {
-                        properties.put("appliesTo", 
toJsonArray(knownType.get().getAppliesTo()));
-                        configDirty = true;
-                        main.getOut().printWarn(
-                                ("Plugin '%s' configuration updated with 
'appliesTo' filter. "
-                                 + "Run 'camel plugin add %s' to refresh the 
full configuration.")
-                                        .formatted(name, name));
-                    }
-                }
-
                 // skip plugins that don't match the target command to avoid 
unnecessary downloads
                 if (target != null && !"shell".equals(target) && 
!target.equals(command)) {
-                    Object appliesToObj = properties.get("appliesTo");
-                    if (!(appliesToObj instanceof Collection<?> appliesToList) 
|| !appliesToList.contains(target)) {
+                    Collection<?> appliesTo = effectiveAppliesTo(properties, 
name);
+                    if (appliesTo != null) {
+                        if (!appliesTo.contains(target)) {
+                            continue;
+                        }
+                    } else if (!PLUGIN_CONSUMING_BUILTINS.contains(target)) {
                         continue;
                     }
                 }
@@ -440,6 +431,16 @@ public final class PluginHelper {
         if (dr.plugin().isPresent() && entry != null && dr.classLoader() != 
null && dr.className() != null) {
             cacheWritten = writeCache(entry, defaultVersion, gav, repos, 
dr.className(), dr.classLoader(), name, depVersion);
         }
+        if (dr.plugin().isPresent() && entry != null && entry.get("appliesTo") 
== null) {
+            CamelJBangPlugin ann = 
dr.plugin().get().getClass().getAnnotation(CamelJBangPlugin.class);
+            if (ann != null && ann.appliesTo().length > 0) {
+                JsonObject resolved = entry.getMap("resolved");
+                if (resolved != null && resolved.get("appliesTo") == null) {
+                    resolved.put("appliesTo", toJsonArray(ann.appliesTo()));
+                    cacheWritten = true;
+                }
+            }
+        }
         return new ResolveResult(dr.plugin(), cacheWritten);
     }
 
@@ -624,6 +625,32 @@ public final class PluginHelper {
         return o == null ? null : o.toString();
     }
 
+    /**
+     * Returns the effective {@code appliesTo} for a plugin entry, checking 
(in order) the top-level config, the
+     * {@code resolved} block from a previous download, and the {@link 
PluginType} enum for known Camel plugins. Returns
+     * {@code null} when no {@code appliesTo} information is available — the 
caller treats this as a legacy plugin that
+     * should be loaded for all targets.
+     */
+    private static Collection<?> effectiveAppliesTo(JsonObject properties, 
String name) {
+        Object obj = properties.get("appliesTo");
+        if (obj instanceof Collection<?> list) {
+            return list;
+        }
+        JsonObject resolved = properties.getMap("resolved");
+        if (resolved != null) {
+            Object resolvedObj = resolved.get("appliesTo");
+            if (resolvedObj instanceof Collection<?> list) {
+                return list;
+            }
+        }
+        Optional<PluginType> knownType = PluginType.findByName(name);
+        if (knownType.isPresent()) {
+            String[] at = knownType.get().getAppliesTo();
+            return at.length > 0 ? Arrays.asList(at) : List.of();
+        }
+        return null;
+    }
+
     public static JsonArray toJsonArray(String[] values) {
         JsonArray array = new JsonArray();
         array.addAll(Arrays.asList(values));
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginFilteringTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginFilteringTest.java
index 7b6948acfe4d..23eb812bd09f 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginFilteringTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/PluginFilteringTest.java
@@ -27,6 +27,7 @@ import java.util.Map;
 import org.aesh.command.Command;
 import org.aesh.command.CommandResult;
 import org.aesh.command.invocation.CommandInvocation;
+import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
 import org.apache.camel.dsl.jbang.core.commands.CliExportContext;
 import org.apache.camel.dsl.jbang.run.common.StringPrinter;
@@ -62,7 +63,7 @@ class PluginFilteringTest {
     void setup() {
         previousHome = CommandLineHelper.getHomeDir();
         CommandLineHelper.useHomeDir(tempDir.toString());
-        camelVersion = new 
org.apache.camel.catalog.DefaultCamelCatalog().getCatalogVersion();
+        camelVersion = new DefaultCamelCatalog().getCatalogVersion();
     }
 
     @AfterEach
@@ -105,30 +106,28 @@ class PluginFilteringTest {
                 .containsKey("beta");
     }
 
-    // ---- The "run" command should not load unrelated plugins ----
+    // ---- Legacy plugins without appliesTo are loaded for built-in commands 
(backward compat) ----
 
     @Test
-    void runCommandDoesNotLoadUnrelatedPlugin() throws Exception {
+    void legacyPluginWithoutAppliesToIsLoadedForRunTarget() throws Exception {
         writeTwoPluginConfig("alpha", "beta");
 
         CamelJBangMain main = new CamelJBangMain();
         Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, 
null, "run");
 
-        assertThat(plugins).doesNotContainKey("alpha")
-                .doesNotContainKey("beta");
+        assertThat(plugins).containsKey("alpha")
+                .containsKey("beta");
     }
 
-    // ---- The "export" command should not load unrelated plugins ----
-
     @Test
-    void exportCommandDoesNotLoadUnrelatedPlugin() throws Exception {
+    void legacyPluginWithoutAppliesToIsLoadedForExportTarget() throws 
Exception {
         writeTwoPluginConfig("alpha", "beta");
 
         CamelJBangMain main = new CamelJBangMain();
         Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, 
null, "export");
 
-        assertThat(plugins).doesNotContainKey("alpha")
-                .doesNotContainKey("beta");
+        assertThat(plugins).containsKey("alpha")
+                .containsKey("beta");
     }
 
     // ---- A broken unrelated plugin should not break the "run" target ----
@@ -203,95 +202,101 @@ class PluginFilteringTest {
     }
 
     @Test
-    void pluginWithAppliesToExportIsLoadedByCliExportContext() throws 
Exception {
-        Path jar = tempDir.resolve("fake-exporter.jar");
-        FakePluginJar.write(jar, "exporter");
+    void pluginWithEmptyAppliesToIsNotLoadedForBuiltinTarget() throws 
Exception {
+        Path jar = tempDir.resolve("fake-alpha.jar");
+        FakePluginJar.write(jar, "alpha");
 
-        JsonObject entry = buildEntry("exporter", camelVersion, jar, 
Files.getLastModifiedTime(jar).toMillis());
-        entry.put("appliesTo", jsonArray("export"));
+        JsonObject entry = buildEntry("alpha", camelVersion, jar, 
Files.getLastModifiedTime(jar).toMillis());
+        entry.put("appliesTo", jsonArray());
         writeConfig(entry);
 
-        StringPrinter printer = new StringPrinter();
-        CamelJBangMain main = new CamelJBangMain().withPrinter(printer);
-
-        ExportContext context = CliExportContext.create(main, printer, true);
+        CamelJBangMain main = new CamelJBangMain();
+        Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, 
null, "export");
 
-        assertThatCode(() -> context.getPlugins(null))
-                .doesNotThrowAnyException();
+        assertThat(plugins).doesNotContainKey("alpha");
     }
 
-    // ---- auto-migration of old configs for known plugins ----
-
     @Test
-    void oldConfigForKnownPluginIsAutoMigrated() throws Exception {
-        Path jar = tempDir.resolve("fake-test.jar");
-        FakePluginJar.write(jar, "test");
-
-        // simulate old config: known plugin "test" without appliesTo
-        JsonObject entry = buildEntry("test", camelVersion, jar, 
Files.getLastModifiedTime(jar).toMillis());
+    void pluginWithAppliesToExportIsLoadedByCliExportContext() throws 
Exception {
+        // A broken entry with appliesTo ["export"]: if CliExportContext 
includes it, resolution
+        // fails — proving the plugin was included in the export target.
+        JsonObject entry = buildBrokenEntry("exporter");
+        entry.put("appliesTo", jsonArray("export"));
         writeConfig(entry);
 
         StringPrinter printer = new StringPrinter();
         CamelJBangMain main = new CamelJBangMain().withPrinter(printer);
 
-        // target "export" — without migration, "test" plugin would be skipped
-        Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, 
null, "export");
-        assertThat(plugins).containsKey("test");
+        ExportContext context = CliExportContext.create(main, printer, true);
 
-        // verify the config was updated on disk
-        JsonObject savedConfig = PluginHelper.getPluginConfig();
-        JsonObject savedPlugins = savedConfig.getMap("plugins");
-        JsonObject savedPlugin = savedPlugins.getMap("test");
-        assertThat(savedPlugin.get("appliesTo"))
-                .isInstanceOf(List.class)
-                .asList().contains("export");
+        assertThatThrownBy(() -> context.getPlugins(null))
+                .isInstanceOf(CliException.class)
+                .hasMessageContaining("camel-jbang-plugin-exporter not found");
     }
 
+    // ---- Known PluginType appliesTo is used at runtime without config entry 
----
+
     @Test
-    void oldConfigForKnownPluginEmitsWarning() throws Exception {
+    void knownPluginTypeAppliesToIsUsedAtRuntime() throws Exception {
         Path jar = tempDir.resolve("fake-test.jar");
         FakePluginJar.write(jar, "test");
 
+        // config entry for "test" plugin without explicit appliesTo
         JsonObject entry = buildEntry("test", camelVersion, jar, 
Files.getLastModifiedTime(jar).toMillis());
         writeConfig(entry);
 
-        StringPrinter printer = new StringPrinter();
-        CamelJBangMain main = new CamelJBangMain().withPrinter(printer);
+        CamelJBangMain main = new CamelJBangMain();
 
-        PluginHelper.getActivePlugins(main, null, "export");
+        // PluginType.TEST declares appliesTo = {"export"}, so it should be 
loaded for "export"
+        Map<String, Plugin> plugins = PluginHelper.getActivePlugins(main, 
null, "export");
+        assertThat(plugins).containsKey("test");
 
-        assertThat(printer.getOutput())
-                .contains("appliesTo");
+        // ... but not for "run"
+        Map<String, Plugin> runPlugins = PluginHelper.getActivePlugins(main, 
null, "run");
+        assertThat(runPlugins).doesNotContainKey("test");
     }
 
-    // ---- CliExportContext should not resolve unrelated plugins ----
+    // ---- CliExportContext should not resolve unrelated plugins (with 
explicit appliesTo) ----
 
     @Test
     void cliExportContextDoesNotResolveUnrelatedPlugins() throws Exception {
         Path goodJar = tempDir.resolve("fake-good.jar");
         FakePluginJar.write(goodJar, "good");
 
-        writeConfig(
-                buildEntry("good", camelVersion, goodJar, 
Files.getLastModifiedTime(goodJar).toMillis()),
-                buildBrokenEntry("broken"));
+        JsonObject goodEntry = buildEntry("good", camelVersion, goodJar, 
Files.getLastModifiedTime(goodJar).toMillis());
+        goodEntry.put("appliesTo", jsonArray("export"));
+
+        // broken plugin with empty appliesTo — explicitly opted out of other 
commands
+        JsonObject brokenEntry = buildBrokenEntry("broken");
+        brokenEntry.put("appliesTo", jsonArray());
+
+        writeConfig(goodEntry, brokenEntry);
 
         StringPrinter printer = new StringPrinter();
         CamelJBangMain main = new CamelJBangMain().withPrinter(printer);
 
         ExportContext context = CliExportContext.create(main, printer, true);
 
-        // CliExportContext.create() passes "export" as target, so the broken 
plugin
-        // (whose command is "broken") is filtered out before resolution.
+        // the broken plugin has appliesTo [] (explicitly empty) so it is 
filtered out
         assertThatCode(() -> context.getPlugins(null))
-                .as("CliExportContext should not resolve plugins unrelated to 
export")
+                .as("CliExportContext should not resolve plugins that opted 
out of export")
                 .doesNotThrowAnyException();
     }
 
-    // ---- addPlugins should not customize unrelated plugins for "run" ----
+    // ---- addPlugins should not customize unrelated plugins ----
 
     @Test
-    void addPluginsForRunDoesNotCustomizeUnrelatedPlugin() throws Exception {
-        writeTwoPluginConfig("alpha", "beta");
+    void addPluginsForRunDoesNotCustomizePluginWithNonMatchingAppliesTo() 
throws Exception {
+        Path jarA = tempDir.resolve("fake-alpha.jar");
+        FakePluginJar.write(jarA, "alpha");
+        Path jarB = tempDir.resolve("fake-beta.jar");
+        FakePluginJar.write(jarB, "beta");
+
+        JsonObject entryA = buildEntry("alpha", camelVersion, jarA, 
Files.getLastModifiedTime(jarA).toMillis());
+        entryA.put("appliesTo", jsonArray("export"));
+        JsonObject entryB = buildEntry("beta", camelVersion, jarB, 
Files.getLastModifiedTime(jarB).toMillis());
+        entryB.put("appliesTo", jsonArray("export"));
+        writeConfig(entryA, entryB);
 
         CamelJBangMain main = new CamelJBangMain().withPrinter(new 
StringPrinter());
         CommandTree commandLine = new CommandTree(main);
@@ -299,16 +304,23 @@ class PluginFilteringTest {
 
         PluginHelper.addPlugins(commandLine, main, "run", "foo.yaml");
 
-        // "run" is a plugin-consuming builtin, but neither plugin's command 
is "run",
-        // so they should be filtered out
         assertThat(commandLine.getSubcommands())
                 .doesNotContainKey("alpha")
                 .doesNotContainKey("beta");
     }
 
     @Test
-    void addPluginsForExportDoesNotCustomizeUnrelatedPlugin() throws Exception 
{
-        writeTwoPluginConfig("alpha", "beta");
+    void addPluginsForExportDoesNotCustomizePluginWithNonMatchingAppliesTo() 
throws Exception {
+        Path jarA = tempDir.resolve("fake-alpha.jar");
+        FakePluginJar.write(jarA, "alpha");
+        Path jarB = tempDir.resolve("fake-beta.jar");
+        FakePluginJar.write(jarB, "beta");
+
+        JsonObject entryA = buildEntry("alpha", camelVersion, jarA, 
Files.getLastModifiedTime(jarA).toMillis());
+        entryA.put("appliesTo", jsonArray("run"));
+        JsonObject entryB = buildEntry("beta", camelVersion, jarB, 
Files.getLastModifiedTime(jarB).toMillis());
+        entryB.put("appliesTo", jsonArray("run"));
+        writeConfig(entryA, entryB);
 
         CamelJBangMain main = new CamelJBangMain().withPrinter(new 
StringPrinter());
         CommandTree commandLine = new CommandTree(main);
@@ -328,15 +340,19 @@ class PluginFilteringTest {
         Path goodJar = tempDir.resolve("fake-good.jar");
         FakePluginJar.write(goodJar, "good");
 
+        // broken plugin with empty appliesTo — explicitly not related to "run"
+        JsonObject brokenEntry = buildBrokenEntry("broken");
+        brokenEntry.put("appliesTo", jsonArray());
+
         writeConfig(
                 buildEntry("good", camelVersion, goodJar, 
Files.getLastModifiedTime(goodJar).toMillis()),
-                buildBrokenEntry("broken"));
+                brokenEntry);
 
         CamelJBangMain main = new CamelJBangMain().withPrinter(new 
StringPrinter());
         CommandTree commandLine = new CommandTree(main);
         commandLine.addSubcommand("run", new CommandTree(new NoOpCommand()));
 
-        // addPlugins for "run" should skip the broken plugin since it doesn't 
match
+        // addPlugins for "run" should skip the broken plugin since its 
appliesTo excludes "run"
         assertThatCode(() -> PluginHelper.addPlugins(commandLine, main, "run", 
"foo.yaml"))
                 .doesNotThrowAnyException();
     }

Reply via email to