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;
+ }
+}