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

royteeuwen pushed a commit to branch feature/multi-feature-and-osgi-dedup
in repository 
https://gitbox.apache.org/repos/asf/sling-feature-launcher-maven-plugin.git

commit 07c0dd40425e477543a7b6001797eb9d77103d57
Author: Roy Teeuwen <[email protected]>
AuthorDate: Tue Jun 2 23:26:27 2026 +0200

    SLING-13233: multi-feature aggregation + OSGi BSN collision detection
    
    Add to <launch>:
    - <features> / <featureFiles> for aggregating multiple input features.
      Order matters for the launcher's LATEST / FIRST overrides.
    - <osgiBsnCollisionDetection> (boolean) maps to the launcher's
      --osgi-bsn-collision-detection flag (SLING-13217 / 1.3.6).
    - <artifactClashOverrides> maps to the launcher's -C flag.
    
    Relax Launch.validate(): combining <feature> + <featureFile> is now
    valid (both contribute as additive inputs). At least one of feature /
    featureFile / features / featureFiles must still be set.
    
    Default featureLauncherVersion bumped from 1.3.4 to 1.3.6 so consumers
    pick up --osgi-bsn-collision-detection without overriding.
---
 .../sling/maven/feature/launcher/Launch.java       | 70 ++++++++++++++++++++--
 .../sling/maven/feature/launcher/StartMojo.java    | 34 +++++++++--
 .../sling/maven/feature/launcher/LaunchTest.java   | 33 +++++++++-
 3 files changed, 125 insertions(+), 12 deletions(-)

diff --git a/src/main/java/org/apache/sling/maven/feature/launcher/Launch.java 
b/src/main/java/org/apache/sling/maven/feature/launcher/Launch.java
index b797316..3c94142 100644
--- a/src/main/java/org/apache/sling/maven/feature/launcher/Launch.java
+++ b/src/main/java/org/apache/sling/maven/feature/launcher/Launch.java
@@ -35,7 +35,11 @@ public class Launch {
 
     private String id;
     private Dependency feature;
+    private List<Dependency> features = new ArrayList<>();
     private String featureFile;
+    private List<String> featureFiles = new ArrayList<>();
+    private boolean osgiBsnCollisionDetection;
+    private List<String> artifactClashOverrides = new ArrayList<>();
     private LauncherArguments launcherArguments = new LauncherArguments();
     private int startTimeoutSeconds = 30;
     private boolean skip = false;
@@ -66,6 +70,44 @@ public class Launch {
         this.featureFile = featureFile;
     }
 
+    /** Additional features (Maven coords) to aggregate alongside {@link 
#getFeature()}. */
+    public List<Dependency> getFeatures() {
+        return features != null ? features : Collections.emptyList();
+    }
+
+    public void setFeatures(List<Dependency> features) {
+        this.features = features;
+    }
+
+    /** Additional feature files to aggregate alongside {@link 
#getFeatureFile()}. */
+    public List<File> getFeatureFiles() {
+        return featureFiles == null
+                ? Collections.emptyList()
+                : 
featureFiles.stream().map(File::new).collect(java.util.stream.Collectors.toList());
+    }
+
+    public void setFeatureFiles(List<String> featureFiles) {
+        this.featureFiles = featureFiles;
+    }
+
+    /** @return whether to enable OSGi BSN collision detection in the 
launcher. Default false. */
+    public boolean isOsgiBsnCollisionDetection() {
+        return osgiBsnCollisionDetection;
+    }
+
+    public void setOsgiBsnCollisionDetection(boolean enabled) {
+        this.osgiBsnCollisionDetection = enabled;
+    }
+
+    /** Maven-coord clash overrides passed to the launcher as {@code -C} 
flags. */
+    public List<String> getArtifactClashOverrides() {
+        return artifactClashOverrides == null ? Collections.emptyList() : 
artifactClashOverrides;
+    }
+
+    public void setArtifactClashOverrides(List<String> artifactClashOverrides) 
{
+        this.artifactClashOverrides = artifactClashOverrides;
+    }
+
     public LauncherArguments getLauncherArguments() {
         return launcherArguments;
     }
@@ -118,17 +160,37 @@ public class Launch {
 
         boolean hasFeature = feature != null;
         boolean hasFeatureFile = featureFile != null && 
!featureFile.trim().isEmpty();
+        boolean hasFeatures = features != null && !features.isEmpty();
+        boolean hasFeatureFiles = featureFiles != null && 
!featureFiles.isEmpty();
 
-        if (hasFeature && hasFeatureFile)
-            throwInvalid("Only one of 'feature' and 'featureFile' is allowed, 
but both are set");
-
-        if (!hasFeature && !hasFeatureFile) throwInvalid("Neither 'feature' 
nor 'featureFile' are set");
+        if (!hasFeature && !hasFeatureFile && !hasFeatures && 
!hasFeatureFiles) {
+            throwInvalid("None of 'feature', 'featureFile', 'features', 
'featureFiles' are set");
+        }
 
         if (hasFeatureFile && !new File(featureFile).exists())
             throwInvalid("Feature file '" + featureFile + "' does not exist");
 
         if (hasFeature && !"slingosgifeature".equals(feature.getType()))
             throwInvalid("type must be 'slingosgifeature' but is '" + 
feature.getType() + "'");
+
+        if (hasFeatures) {
+            for (final Dependency d : features) {
+                if (!"slingosgifeature".equals(d.getType())) {
+                    throwInvalid("features[].type must be 'slingosgifeature' 
but is '" + d.getType() + "'");
+                }
+            }
+        }
+
+        if (hasFeatureFiles) {
+            for (final String f : featureFiles) {
+                if (f == null || f.trim().isEmpty()) {
+                    throwInvalid("featureFiles[] entry is blank");
+                }
+                if (!new File(f).exists()) {
+                    throwInvalid("Feature file '" + f + "' does not exist");
+                }
+            }
+        }
     }
 
     private void throwInvalid(String reason) {
diff --git 
a/src/main/java/org/apache/sling/maven/feature/launcher/StartMojo.java 
b/src/main/java/org/apache/sling/maven/feature/launcher/StartMojo.java
index ad2fd4f..dfc7493 100644
--- a/src/main/java/org/apache/sling/maven/feature/launcher/StartMojo.java
+++ b/src/main/java/org/apache/sling/maven/feature/launcher/StartMojo.java
@@ -81,7 +81,7 @@ public class StartMojo extends AbstractMojo {
     /**
      * The version of the <a 
href="https://github.com/apache/sling-org-apache-sling-feature-launcher";>Sling 
Feature Launcher</a> to use.
      */
-    @Parameter(required = true, defaultValue = "1.3.4")
+    @Parameter(required = true, defaultValue = "1.3.6")
     private String featureLauncherVersion;
 
     /**
@@ -215,12 +215,23 @@ public class StartMojo extends AbstractMojo {
 
                 launch.validate();
 
-                File featureFile = launch.getFeature()
+                // Aggregate every primary + additional feature into a single 
ordered list
+                // of files to pass to the launcher CLI as separate `-f` 
flags. Order
+                // matters: it determines which feature wins under FIRST_WINS 
/ LAST_WINS
+                // and how merge handlers see the input.
+                final List<File> featureFiles = new ArrayList<>();
+                launch.getFeature()
                         .map(this::toArtifact)
                         .map(a -> uncheckedResolveArtifact(repositorySession, 
a))
                         .map(r -> r.getArtifact().getFile())
-                        .orElseGet(() -> launch.getFeatureFile()
-                                .get()); // the Launch is guaranteed to either 
have a feature or a featureFile set
+                        .ifPresent(featureFiles::add);
+                launch.getFeatureFile().ifPresent(featureFiles::add);
+                for (org.apache.maven.model.Dependency d : 
launch.getFeatures()) {
+                    
featureFiles.add(uncheckedResolveArtifact(repositorySession, toArtifact(d))
+                            .getArtifact()
+                            .getFile());
+                }
+                featureFiles.addAll(launch.getFeatureFiles());
 
                 String javahome = System.getenv(JAVA_HOME);
                 if (javahome == null || javahome.isEmpty()) {
@@ -300,11 +311,22 @@ public class StartMojo extends AbstractMojo {
                 repositoryUrls.forEach(joiner::add);
                 args.add(joiner.toString());
 
-                args.add("-f");
-                args.add(featureFile.getAbsolutePath());
+                for (File f : featureFiles) {
+                    args.add("-f");
+                    args.add(f.getAbsolutePath());
+                }
                 args.add("-p");
                 args.add(launch.getId());
 
+                if (launch.isOsgiBsnCollisionDetection()) {
+                    args.add("--osgi-bsn-collision-detection");
+                }
+
+                for (String override : launch.getArtifactClashOverrides()) {
+                    args.add("-C");
+                    args.add(override);
+                }
+
                 for (Map.Entry<String, String> frameworkProperty :
                         
launch.getLauncherArguments().getFrameworkProperties().entrySet()) {
                     args.add("-D");
diff --git 
a/src/test/java/org/apache/sling/maven/feature/launcher/LaunchTest.java 
b/src/test/java/org/apache/sling/maven/feature/launcher/LaunchTest.java
index a41dac3..e67152a 100644
--- a/src/test/java/org/apache/sling/maven/feature/launcher/LaunchTest.java
+++ b/src/test/java/org/apache/sling/maven/feature/launcher/LaunchTest.java
@@ -112,9 +112,12 @@ public class LaunchTest {
         launch.validate();
     }
 
-    @Test(expected = IllegalArgumentException.class)
-    public void invalidLaunch_bothFeatureAndFeatureFile() {
+    @Test
+    public void validLaunch_bothFeatureAndFeatureFile() {
 
+        // Combining a feature (Maven coords) and a featureFile (local file 
path) is now
+        // allowed — they are both additive inputs to the launcher's feature 
aggregation,
+        // not mutually exclusive single-feature selectors.
         Launch launch = new Launch();
         launch.setId("feature");
         launch.setFeature(validDep);
@@ -130,4 +133,30 @@ public class LaunchTest {
         launch.setFeatureFile(validFeatureFile.getAbsolutePath() + ".missing");
         launch.validate();
     }
+
+    @Test
+    public void validLaunch_multipleFeatures() {
+        Launch launch = new Launch();
+        launch.setId("feature");
+        launch.setFeatures(java.util.Arrays.asList(validDep, validDep));
+        launch.validate();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void invalidLaunch_missingEntryInFeatureFiles() {
+        Launch launch = new Launch();
+        launch.setId("feature");
+        launch.setFeatureFiles(java.util.Arrays.asList(
+                validFeatureFile.getAbsolutePath(), 
validFeatureFile.getAbsolutePath() + ".missing"));
+        launch.validate();
+    }
+
+    @Test
+    public void validLaunch_osgiBsnCollisionDetection() {
+        Launch launch = new Launch();
+        launch.setId("feature");
+        launch.setFeatureFile(validFeatureFile.getAbsolutePath());
+        launch.setOsgiBsnCollisionDetection(true);
+        launch.validate();
+    }
 }

Reply via email to