From 50021783931eee7f8f0ae996ec787024e0236455 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?JB=20Onofr=C3=A9?= <jb.onofre@dremio.com>
Date: Sun, 5 Apr 2026 18:34:52 +0200
Subject: [PATCH] fix(karaf-maven-plugin): Deal with non-features classifier in
 the karaf-maven-plugin

---
 .../features/GenerateDescriptorMojo.java      | 32 +++++++++++++++----
 .../tooling/utils/Dependency31Helper.java     | 32 ++++++++++++++++++-
 2 files changed, 56 insertions(+), 8 deletions(-)

diff --git a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/features/GenerateDescriptorMojo.java b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/features/GenerateDescriptorMojo.java
index 63575529691..f49fe33ed75 100644
--- a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/features/GenerateDescriptorMojo.java
+++ b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/features/GenerateDescriptorMojo.java
@@ -26,9 +26,11 @@
 import java.util.Collection;
 import java.util.Comparator;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.jar.JarInputStream;
 import java.util.jar.Manifest;
 
@@ -37,6 +39,7 @@
 import javax.xml.stream.XMLStreamException;
 
 import org.apache.karaf.features.internal.model.*;
+import org.apache.karaf.tooling.utils.Dependency31Helper;
 import org.apache.karaf.tooling.utils.DependencyHelper;
 import org.apache.karaf.tooling.utils.DependencyHelperFactory;
 import org.apache.karaf.tooling.utils.LocalDependency;
@@ -490,6 +493,7 @@ private void writeFeatures(PrintStream out) throws ArtifactResolutionException,
         // TODO Initialise the repositories from the existing feature file if any
         Map<Dependency, Feature> otherFeatures = new HashMap<>();
         Map<Feature, String> featureRepositories = new HashMap<>();
+        Set<Object> recognizedFeatures = new HashSet<>();
         FeaturesCache cache = new FeaturesCache(featuresCacheSize, artifactCacheSize);
         for (final LocalDependency entry : localDependencies) {
             Object artifact = entry.getArtifact();
@@ -499,7 +503,7 @@ private void writeFeatures(PrintStream out) throws ArtifactResolutionException,
             }
 
             processFeatureArtifact(features, feature, otherFeatures, featureRepositories, cache, artifact,
-                    entry.getParent(), true);
+                    entry.getParent(), true, recognizedFeatures);
         }
         // Do not retain cache beyond this point
         cache = null;
@@ -514,7 +518,7 @@ private void writeFeatures(PrintStream out) throws ArtifactResolutionException,
                     continue;
                 }
 
-                if (!this.dependencyHelper.isArtifactAFeature(artifact)) {
+                if (!this.dependencyHelper.isArtifactAFeature(artifact) && !recognizedFeatures.contains(artifact)) {
                     String bundleName = this.dependencyHelper.artifactToMvn(artifact, getVersionOrRange(entry.getParent(), artifact));
 
                     for (ConfigFile cf : feature.getConfigfile()) {
@@ -609,11 +613,25 @@ private void writeFeatures(PrintStream out) throws ArtifactResolutionException,
 
     private void processFeatureArtifact(Features features, Feature feature, Map<Dependency, Feature> otherFeatures,
                                         Map<Feature, String> featureRepositories, FeaturesCache cache,
-                                        Object artifact, Object parent, boolean add)
+                                        Object artifact, Object parent, boolean add, Set<Object> recognizedFeatures)
             throws MojoExecutionException, XMLStreamException, JAXBException, IOException {
-        if (this.dependencyHelper.isArtifactAFeature(artifact) && FEATURE_CLASSIFIER.equals(
-                this.dependencyHelper.getClassifier(artifact))) {
-            File featuresFile = this.dependencyHelper.resolve(artifact, getLog());
+        boolean isFeature = this.dependencyHelper.isArtifactAFeature(artifact);
+        File featuresFile = null;
+        if (!isFeature) {
+            // For XML artifacts with non-standard classifiers, resolve and check content
+            featuresFile = this.dependencyHelper.resolve(artifact, getLog());
+            if (featuresFile != null && featuresFile.exists()
+                    && Dependency31Helper.isFeaturesXml(featuresFile)) {
+                isFeature = true;
+            }
+        }
+        if (isFeature) {
+            if (recognizedFeatures != null) {
+                recognizedFeatures.add(artifact);
+            }
+            if (featuresFile == null) {
+                featuresFile = this.dependencyHelper.resolve(artifact, getLog());
+            }
             if (featuresFile == null || !featuresFile.exists()) {
                 throw new MojoExecutionException(
                         "Cannot locate file for feature: " + artifact + " at " + featuresFile);
@@ -621,7 +639,7 @@ private void processFeatureArtifact(Features features, Feature feature, Map<Depe
             Features includedFeatures = cache.getFeature(featuresFile);
             for (String repository : includedFeatures.getRepository()) {
                 processFeatureArtifact(features, feature, otherFeatures, featureRepositories, cache,
-                        cache.getArtifact(repository), parent, false);
+                        cache.getArtifact(repository), parent, false, recognizedFeatures);
             }
             for (Feature includedFeature : includedFeatures.getFeature()) {
                 Dependency dependency = new Dependency(includedFeature.getName(), includedFeature.getVersion());
diff --git a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/utils/Dependency31Helper.java b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/utils/Dependency31Helper.java
index 44bd3d0fa2e..497a8e7465b 100644
--- a/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/utils/Dependency31Helper.java
+++ b/tooling/karaf-maven-plugin/src/main/java/org/apache/karaf/tooling/utils/Dependency31Helper.java
@@ -43,10 +43,16 @@
 import org.codehaus.plexus.util.StringUtils;
 
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.*;
 
+import javax.xml.namespace.QName;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamReader;
+
 import static java.lang.String.*;
 import static org.apache.commons.lang3.reflect.MethodUtils.invokeMethod;
 import static org.apache.karaf.deployer.kar.KarArtifactInstaller.FEATURE_CLASSIFIER;
@@ -284,7 +290,31 @@ public static boolean isFeature(DependencyNode dependencyNode) {
     }
 
     public static boolean isFeature(Artifact artifact) {
-        return artifact.getExtension().equals("kar") || FEATURE_CLASSIFIER.equals(artifact.getClassifier());
+        if (artifact.getExtension().equals("kar") || FEATURE_CLASSIFIER.equals(artifact.getClassifier())) {
+            return true;
+        }
+        // For XML artifacts with non-standard classifiers, check actual content when the file is available
+        if ("xml".equals(artifact.getExtension()) && artifact.getFile() != null && artifact.getFile().exists()) {
+            return isFeaturesXml(artifact.getFile());
+        }
+        return false;
+    }
+
+    public static boolean isFeaturesXml(File file) {
+        try (InputStream is = new FileInputStream(file)) {
+            XMLInputFactory xif = XMLInputFactory.newFactory();
+            xif.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, true);
+            xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
+            xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
+            XMLStreamReader r = xif.createXMLStreamReader(is);
+            r.nextTag();
+            QName name = r.getName();
+            return name.getLocalPart().equals("features")
+                    && (name.getNamespaceURI().isEmpty()
+                            || name.getNamespaceURI().startsWith("http://karaf.apache.org/xmlns/features/"));
+        } catch (Exception e) {
+            return false;
+        }
     }
 
     @Override
