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

apupier 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 1a0cd7c3c0c8 CAMEL-25136: avoid ClassCastException when interface 
files are scanned in commands dir
1a0cd7c3c0c8 is described below

commit 1a0cd7c3c0c8f492003574db56826aa80febf017
Author: Aurélien Pupier <[email protected]>
AuthorDate: Wed Oct 14 11:46:52 2026 +0200

    CAMEL-25136: avoid ClassCastException when interface files are scanned
    in commands dir
    
    When PrepareCamelJBangCommandsMojo scans the commands directory it walks
    all .java files, including interfaces (e.g. BindingProvider,
    TemplateProvider,
    Update, MavenResolverMixinSpec, QuarkusPlatformMixinSpec). Three places
    in
    parseOptionsFromClassHierarchy and resolveCompletionCandidates performed
    an
    unchecked cast of Roaster.parse() result to JavaClassSource. When
    Roaster
    returns a JavaInterfaceImpl for an interface file the cast threw a
    ClassCastException that was caught by the outer try/catch and logged as
    a
    WARNING.
    
    Fix: replace each unchecked cast with an instanceof pattern-match guard.
    Non-class types (interfaces, enums, annotations) are now silently
    skipped
    at DEBUG level with an explanatory message instead of producing a
    WARNING.
    
    Affected call sites:
    - parent class file lookup in parseOptionsFromClassHierarchy
    - @Mixin field type file lookup in parseOptionsFromClassHierarchy
    - completionCandidates class file lookup in resolveCompletionCandidates
    
    A new test class PrepareCamelJBangCommandsMojoTest is added with two
    non-regression scenarios:
    - interface used as @Mixin type alongside a command class
    - interface file co-located with a concrete command class
    
    Co-authored-by: IBM Bob 2.2.1
    Signed-off-by: Aurélien Pupier <[email protected]>
---
 .../packaging/PrepareCamelJBangCommandsMojo.java   | 101 ++++++----
 .../PrepareCamelJBangCommandsMojoTest.java         | 215 +++++++++++++++++++++
 2 files changed, 274 insertions(+), 42 deletions(-)

diff --git 
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojo.java
 
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojo.java
index 3e111e3a33b5..6cd3b9952277 100644
--- 
a/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojo.java
+++ 
b/tooling/maven/camel-package-maven-plugin/src/main/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojo.java
@@ -45,6 +45,7 @@ import org.apache.maven.project.MavenProject;
 import org.apache.maven.project.MavenProjectHelper;
 import org.codehaus.plexus.build.BuildContext;
 import org.jboss.forge.roaster.Roaster;
+import org.jboss.forge.roaster.model.JavaType;
 import org.jboss.forge.roaster.model.source.AnnotationSource;
 import org.jboss.forge.roaster.model.source.FieldSource;
 import org.jboss.forge.roaster.model.source.JavaClassSource;
@@ -429,51 +430,55 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
     }
 
     private CommandInfo parseCommandClass(File file) throws IOException {
-        JavaClassSource clazz = (JavaClassSource) Roaster.parse(file);
+        JavaType<?> parsedFile = Roaster.parse(file);
+        if (parsedFile instanceof JavaClassSource clazz) {
 
-        // Check if this class has @Command annotation
-        AnnotationSource<?> commandAnn = findAnnotation(clazz, "Command");
-        if (commandAnn == null) {
-            return null;
-        }
+            // Check if this class has @Command annotation
+            AnnotationSource<?> commandAnn = findAnnotation(clazz, "Command");
+            if (commandAnn == null) {
+                return null;
+            }
 
-        CommandInfo info = new CommandInfo();
-        info.className = clazz.getName();
-        info.qualifiedName = clazz.getQualifiedName();
+            CommandInfo info = new CommandInfo();
+            info.className = clazz.getName();
+            info.qualifiedName = clazz.getQualifiedName();
 
-        // Parse @Command annotation
-        String name = commandAnn.getStringValue("name");
-        if (name != null) {
-            info.name = name.replace("\"", "");
-        } else {
-            info.name = clazz.getName().toLowerCase();
-        }
+            // Parse @Command annotation
+            String name = commandAnn.getStringValue("name");
+            if (name != null) {
+                info.name = name.replace("\"", "");
+            } else {
+                info.name = clazz.getName().toLowerCase();
+            }
 
-        String description = commandAnn.getStringValue("description");
-        if (description != null) {
-            description = description.replace("\"", "");
-            // Clean up description - remove help hints
-            if (description.contains("(use")) {
-                description = description.substring(0, 
description.indexOf("(use")).trim();
+            String description = commandAnn.getStringValue("description");
+            if (description != null) {
+                description = description.replace("\"", "");
+                // Clean up description - remove help hints
+                if (description.contains("(use")) {
+                    description = description.substring(0, 
description.indexOf("(use")).trim();
+                }
+                info.description = description;
             }
-            info.description = description;
-        }
 
-        // Check for deprecated
-        if (clazz.getAnnotation(Deprecated.class) != null) {
-            info.deprecated = true;
-        }
+            // Check for deprecated
+            if (clazz.getAnnotation(Deprecated.class) != null) {
+                info.deprecated = true;
+            }
 
-        // Check for hidden
-        if ("true".equals(commandAnn.getStringValue("hidden"))) {
-            info.hidden = true;
-        }
+            // Check for hidden
+            if ("true".equals(commandAnn.getStringValue("hidden"))) {
+                info.hidden = true;
+            }
 
-        // Parse options from this class AND parent classes
-        info.options = new ArrayList<>();
-        parseOptionsFromClassHierarchy(clazz, info.options, 
file.getParentFile());
+            // Parse options from this class AND parent classes
+            info.options = new ArrayList<>();
+            parseOptionsFromClassHierarchy(clazz, info.options, 
file.getParentFile());
 
-        return info;
+            return info;
+        } else {
+            return null;
+        }
     }
 
     private void parseOptionsFromClassHierarchy(JavaClassSource clazz, 
List<OptionInfo> options, File baseDir) {
@@ -490,8 +495,12 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
             File parentFile = findClassFile(baseDir, parentClassName);
             if (parentFile != null && parentFile.exists()) {
                 try {
-                    JavaClassSource parentClazz = (JavaClassSource) 
Roaster.parse(parentFile);
-                    parseOptionsFromClassHierarchy(parentClazz, options, 
parentFile.getParentFile());
+                    JavaType<?> parsedParent = Roaster.parse(parentFile);
+                    if (parsedParent instanceof JavaClassSource parentClazz) {
+                        parseOptionsFromClassHierarchy(parentClazz, options, 
parentFile.getParentFile());
+                    } else {
+                        getLog().debug("Skipping options from non-class parent 
type: " + parentClassName);
+                    }
                 } catch (Exception e) {
                     getLog().debug("Could not parse parent class: " + 
parentClassName + " - " + e.getMessage());
                 }
@@ -529,8 +538,12 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
                 File mixinFile = findClassFile(baseDir, mixinTypeName);
                 if (mixinFile != null && mixinFile.exists()) {
                     try {
-                        JavaClassSource mixinClazz = (JavaClassSource) 
Roaster.parse(mixinFile);
-                        parseOptionsFromClassHierarchy(mixinClazz, options, 
mixinFile.getParentFile());
+                        JavaType<?> parsedMixin = Roaster.parse(mixinFile);
+                        if (parsedMixin instanceof JavaClassSource mixinClazz) 
{
+                            parseOptionsFromClassHierarchy(mixinClazz, 
options, mixinFile.getParentFile());
+                        } else {
+                            getLog().debug("Skipping options from non-class 
mixin type: " + mixinTypeName);
+                        }
                     } catch (Exception e) {
                         getLog().debug("Could not parse mixin class: " + 
mixinTypeName + " - " + e.getMessage());
                     }
@@ -761,8 +774,12 @@ public class PrepareCamelJBangCommandsMojo extends 
AbstractGeneratorMojo {
 
         if (classFile != null) {
             try {
-                JavaClassSource candidatesClass = (JavaClassSource) 
Roaster.parse(classFile);
-                return extractCandidateValues(candidatesClass);
+                JavaType<?> parsedCandidates = Roaster.parse(classFile);
+                if (parsedCandidates instanceof JavaClassSource 
candidatesClass) {
+                    return extractCandidateValues(candidatesClass);
+                } else {
+                    getLog().debug("Skipping completion candidates from 
non-class type: " + className);
+                }
             } catch (Exception e) {
                 getLog().debug("Could not parse completion candidates class: " 
+ className + " - " + e.getMessage());
             }
diff --git 
a/tooling/maven/camel-package-maven-plugin/src/test/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojoTest.java
 
b/tooling/maven/camel-package-maven-plugin/src/test/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojoTest.java
new file mode 100644
index 000000000000..582e47054fa1
--- /dev/null
+++ 
b/tooling/maven/camel-package-maven-plugin/src/test/java/org/apache/camel/maven/packaging/PrepareCamelJBangCommandsMojoTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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.maven.packaging;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.maven.model.Build;
+import org.apache.maven.plugin.logging.Log;
+import org.apache.maven.plugin.logging.SystemStreamLog;
+import org.apache.maven.project.MavenProject;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Non-regression test for CAMEL-25136: interfaces (JavaInterfaceImpl) 
encountered while scanning command classes must
+ * not produce ClassCastException warnings.
+ */
+class PrepareCamelJBangCommandsMojoTest {
+
+    @TempDir
+    Path tempDir;
+
+    /**
+     * Verifies that an interface file co-located in the commands directory — 
used as a mixin type — does not produce a
+     * WARNING log. Before the fix, Roaster.parse() returned JavaInterfaceImpl 
and the unchecked cast to JavaClassSource
+     * threw a ClassCastException caught as WARN.
+     */
+    @Test
+    void testInterfaceMixinDoesNotProduceWarning() throws Exception {
+        // Write a minimal interface (the mixin type) alongside the command 
class
+        Files.writeString(tempDir.resolve("TestMixinSpec.java"), """
+                package org.apache.camel.dsl.jbang.core.commands;
+
+                public interface TestMixinSpec {
+                    String repos();
+                    boolean download();
+                }
+                """);
+
+        // Write a command class whose mixin field type is the interface above
+        Files.writeString(tempDir.resolve("TestCommand.java"), """
+                package org.apache.camel.dsl.jbang.core.commands;
+
+                import picocli.CommandLine;
+                import picocli.CommandLine.Command;
+                import picocli.CommandLine.Option;
+                import picocli.CommandLine.Mixin;
+
+                @Command(name = "test", description = "A test command")
+                public class TestCommand {
+
+                    @Mixin
+                    TestMixinSpec mixin;
+
+                    @Option(names = "--verbose", description = "Enable verbose 
output")
+                    boolean verbose;
+                }
+                """);
+
+        // Write a minimal CamelJBangMain.java referencing TestCommand
+        Files.writeString(tempDir.resolve("CamelJBangMain.java"), """
+                package org.apache.camel.dsl.jbang.core.commands;
+
+                import picocli.CommandLine;
+                import picocli.CommandLine.Command;
+
+                @Command(name = "camel")
+                public class CamelJBangMain {
+                    public static void main(String[] args) {
+                        CommandLine commandLine;
+                        commandLine = new CommandLine(new CamelJBangMain())
+                                .addSubcommand("test", new CommandLine(new 
TestCommand()));
+                    }
+                }
+                """);
+
+        // Capture warnings via a custom Log
+        List<String> warnings = new ArrayList<>();
+        Log capturingLog = new SystemStreamLog() {
+            @Override
+            public void warn(CharSequence content) {
+                warnings.add(content.toString());
+                super.warn(content);
+            }
+
+            @Override
+            public void warn(CharSequence content, Throwable error) {
+                warnings.add(content.toString());
+                super.warn(content, error);
+            }
+        };
+
+        PrepareCamelJBangCommandsMojo mojo = new 
PrepareCamelJBangCommandsMojo(null, null);
+        mojo.setLog(capturingLog);
+        mojo.commandsDir = tempDir.toFile();
+        mojo.outFolder = tempDir.resolve("generated-resources").toFile();
+        mojo.project = 
projectWithOutputDir(tempDir.resolve("classes").toFile());
+
+        mojo.execute();
+
+        // The metadata file must have been written
+        File metadataFile = tempDir.resolve("generated-resources")
+                
.resolve(PrepareCamelJBangCommandsMojo.CORE_METADATA_FILE).toFile();
+        assertThat(metadataFile).exists();
+        assertThat(metadataFile.length()).isGreaterThan(0);
+
+        // No warnings should have been emitted for the interface file
+        assertThat(warnings)
+                .as("No warnings expected when an interface is encountered in 
the commands directory")
+                .isEmpty();
+    }
+
+    /**
+     * Verifies that an interface used as a supertype (parent class) in the 
hierarchy does not produce a WARNING.
+     */
+    @Test
+    void testInterfaceParentTypeDoesNotProduceWarning() throws Exception {
+        // A pure interface used as a "parent" (supertype lookup will find 
this file)
+        Files.writeString(tempDir.resolve("TestParentInterface.java"), """
+                package org.apache.camel.dsl.jbang.core.commands;
+
+                public interface TestParentInterface {
+                    void run();
+                }
+                """);
+
+        Files.writeString(tempDir.resolve("TestChildCommand.java"), """
+                package org.apache.camel.dsl.jbang.core.commands;
+
+                import picocli.CommandLine.Command;
+                import picocli.CommandLine.Option;
+
+                @Command(name = "child", description = "A child command")
+                public class TestChildCommand implements TestParentInterface {
+
+                    @Option(names = "--output", description = "Output format")
+                    String output;
+
+                    public void run() {}
+                }
+                """);
+
+        Files.writeString(tempDir.resolve("CamelJBangMain.java"), """
+                package org.apache.camel.dsl.jbang.core.commands;
+
+                import picocli.CommandLine;
+                import picocli.CommandLine.Command;
+
+                @Command(name = "camel")
+                public class CamelJBangMain {
+                    public static void main(String[] args) {
+                        CommandLine commandLine;
+                        commandLine = new CommandLine(new CamelJBangMain())
+                                .addSubcommand("child", new CommandLine(new 
TestChildCommand()));
+                    }
+                }
+                """);
+
+        List<String> warnings = new ArrayList<>();
+        Log capturingLog = new SystemStreamLog() {
+            @Override
+            public void warn(CharSequence content) {
+                warnings.add(content.toString());
+                super.warn(content);
+            }
+
+            @Override
+            public void warn(CharSequence content, Throwable error) {
+                warnings.add(content.toString());
+                super.warn(content, error);
+            }
+        };
+
+        PrepareCamelJBangCommandsMojo mojo = new 
PrepareCamelJBangCommandsMojo(null, null);
+        mojo.setLog(capturingLog);
+        mojo.commandsDir = tempDir.toFile();
+        mojo.outFolder = tempDir.resolve("generated-resources").toFile();
+        mojo.project = 
projectWithOutputDir(tempDir.resolve("classes").toFile());
+
+        mojo.execute();
+
+        assertThat(warnings)
+                .as("No warnings expected when an interface is encountered as 
a parent type")
+                .isEmpty();
+    }
+
+    private static MavenProject projectWithOutputDir(File outputDir) {
+        outputDir.mkdirs();
+        Build build = new Build();
+        build.setOutputDirectory(outputDir.getAbsolutePath());
+        MavenProject project = new MavenProject();
+        project.setBuild(build);
+        return project;
+    }
+}

Reply via email to