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

royteeuwen pushed a commit to branch feature/additional-feature-files
in repository 
https://gitbox.apache.org/repos/asf/sling-slingfeature-maven-plugin.git

commit 26462b1e7aa512da9612212eab5c1154f519ed52
Author: Roy Teeuwen <[email protected]>
AuthorDate: Wed Jun 3 21:04:16 2026 +0200

    SLING-13233: aggregate features from goal-time paths with optional OSGi BSN 
collision detection
    
    Add two parameters to `<aggregate>` in 
`slingfeature-maven-plugin:aggregate-features`:
    
    * `<additionalFeatureFiles>` — a list of feature JSON files read at goal 
time and
      merged into the selection. Unlike `<filesInclude>`, these paths bypass the
      session-start Preprocessor scan, so they can reference outputs produced 
earlier
      in the same build (e.g. cpconverter JSON under `target/`).
    * `<osgiBsnCollisionDetection>` — when `true`, the aggregator calls
      `BuilderContext.setOsgiBsnCollisionDetection(true)` (requires
      `org.apache.sling.feature` 2.0.6+) so the build fails when two bundles 
share
      the same OSGi `Bundle-SymbolicName`+`Bundle-Version` across different 
Maven
      coordinates unless an existing `<artifactsOverrides>` rule resolves them.
    
    This is an alternative aggregation flow to the launcher-plugin parameters 
added
    in https://github.com/apache/sling-feature-launcher-maven-plugin/pull/32; 
both
    produce the same on-disk aggregated feature, but the 
slingfeature-maven-plugin
    flow exposes the result to downstream goals in the same build.
    
    Bumps `org.apache.sling.feature` 2.0.0 → 2.0.6 for the new BuilderContext 
API.
    
    Tests: 3 new tests covering merge, missing-file, and collision-flag 
propagation.
    20/20 existing unit tests still pass; 16/16 integration tests still pass.
---
 pom.xml                                            |  2 +-
 .../sling/feature/maven/mojos/Aggregate.java       | 27 +++++++++-
 .../feature/maven/mojos/AggregateFeaturesMojo.java | 37 ++++++++++++++
 .../maven/mojos/AggregateFeaturesMojoTest.java     | 58 ++++++++++++++++++++++
 4 files changed, 121 insertions(+), 3 deletions(-)

diff --git a/pom.xml b/pom.xml
index b351f6e..aa55da9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -48,7 +48,7 @@
         <maven.scm.version>1.11.2</maven.scm.version>
         
<maven.site.path>${project.artifactId}-archives/${project.artifactId}-LATEST</maven.site.path>
         
<project.build.outputTimestamp>1771315797</project.build.outputTimestamp>
-        <sling.feature.version>2.0.0</sling.feature.version>
+        <sling.feature.version>2.0.6</sling.feature.version>
     </properties>
 
     <dependencies>
diff --git a/src/main/java/org/apache/sling/feature/maven/mojos/Aggregate.java 
b/src/main/java/org/apache/sling/feature/maven/mojos/Aggregate.java
index 1610b99..8ab55b2 100644
--- a/src/main/java/org/apache/sling/feature/maven/mojos/Aggregate.java
+++ b/src/main/java/org/apache/sling/feature/maven/mojos/Aggregate.java
@@ -18,6 +18,7 @@
  */
 package org.apache.sling.feature.maven.mojos;
 
+import java.io.File;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -74,6 +75,23 @@ public class Aggregate extends FeatureSelectionConfig {
 
     public Map<String, String> frameworkPropertiesOverrides;
 
+    /**
+     * Additional feature files to include in the aggregation, resolved at 
goal time
+     * rather than through the session-start Preprocessor scan. Use this to 
pull in
+     * feature JSONs that are produced by another plugin earlier in the same 
build
+     * (e.g. cpconverter output under <code>target/</code>) which therefore do 
not
+     * exist when the lifecycle participant runs.
+     */
+    public List<File> additionalFeatureFiles;
+
+    /**
+     * When {@code true}, the aggregated feature is checked for OSGi bundles 
with
+     * duplicate {@code Bundle-SymbolicName} values and the build fails unless 
an
+     * {@code artifactsOverrides} rule resolves the collision. Requires
+     * {@code org.apache.sling.feature} 2.0.6 or later.
+     */
+    public boolean osgiBsnCollisionDetection = false;
+
     /* (non-Javadoc)
      * @see java.lang.Object#hashCode()
      */
@@ -81,6 +99,7 @@ public class Aggregate extends FeatureSelectionConfig {
     public int hashCode() {
         return Objects.hash(
                 super.hashCode(),
+                additionalFeatureFiles,
                 artifactsOverrides,
                 attach,
                 classifier,
@@ -89,6 +108,7 @@ public class Aggregate extends FeatureSelectionConfig {
                 frameworkPropertiesOverrides,
                 markAsComplete,
                 markAsFinal,
+                osgiBsnCollisionDetection,
                 title,
                 variablesOverrides,
                 vendor);
@@ -103,7 +123,8 @@ public class Aggregate extends FeatureSelectionConfig {
         if (!super.equals(obj)) return false;
         if (getClass() != obj.getClass()) return false;
         Aggregate other = (Aggregate) obj;
-        return Objects.equals(artifactsOverrides, other.artifactsOverrides)
+        return Objects.equals(additionalFeatureFiles, 
other.additionalFeatureFiles)
+                && Objects.equals(artifactsOverrides, other.artifactsOverrides)
                 && attach == other.attach
                 && Objects.equals(classifier, other.classifier)
                 && Objects.equals(configurationOverrides, 
other.configurationOverrides)
@@ -111,6 +132,7 @@ public class Aggregate extends FeatureSelectionConfig {
                 && Objects.equals(frameworkPropertiesOverrides, 
other.frameworkPropertiesOverrides)
                 && markAsComplete == other.markAsComplete
                 && markAsFinal == other.markAsFinal
+                && osgiBsnCollisionDetection == other.osgiBsnCollisionDetection
                 && Objects.equals(title, other.title)
                 && Objects.equals(variablesOverrides, other.variablesOverrides)
                 && Objects.equals(vendor, other.vendor);
@@ -123,7 +145,8 @@ public class Aggregate extends FeatureSelectionConfig {
                 + ", markAsFinal=" + markAsFinal + ", markAsComplete=" + 
markAsComplete + ", title=" + title
                 + ", description=" + description + ", vendor=" + vendor + ", 
artifactsOverrides=" + artifactsOverrides
                 + ", variablesOverrides=" + variablesOverrides + ", 
frameworkPropertiesOverrides="
-                + frameworkPropertiesOverrides + "]";
+                + frameworkPropertiesOverrides + ", additionalFeatureFiles=" + 
additionalFeatureFiles
+                + ", osgiBsnCollisionDetection=" + osgiBsnCollisionDetection + 
"]";
     }
 
     public List<ArtifactId> getArtifactOverrideRules() {
diff --git 
a/src/main/java/org/apache/sling/feature/maven/mojos/AggregateFeaturesMojo.java 
b/src/main/java/org/apache/sling/feature/maven/mojos/AggregateFeaturesMojo.java
index a511280..46c9e4c 100644
--- 
a/src/main/java/org/apache/sling/feature/maven/mojos/AggregateFeaturesMojo.java
+++ 
b/src/main/java/org/apache/sling/feature/maven/mojos/AggregateFeaturesMojo.java
@@ -18,6 +18,11 @@
  */
 package org.apache.sling.feature.maven.mojos;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -40,6 +45,7 @@ import org.apache.sling.feature.builder.BuilderContext;
 import org.apache.sling.feature.builder.FeatureBuilder;
 import org.apache.sling.feature.builder.MergeHandler;
 import org.apache.sling.feature.builder.PostProcessHandler;
+import org.apache.sling.feature.io.json.FeatureJSONReader;
 import org.apache.sling.feature.maven.FeatureConstants;
 import org.apache.sling.feature.maven.ProjectHelper;
 
@@ -113,6 +119,14 @@ public class AggregateFeaturesMojo extends 
AbstractIncludingFeatureMojo {
             ProjectHelper.validateFeatureClassifiers(this.project, 
aggregate.classifier, aggregate.attach);
 
             final Map<String, Feature> selection = 
this.getSelectedFeatures(aggregate);
+
+            // Load feature files declared via <additionalFeatureFiles>. These 
are read at goal
+            // time so they can reference outputs produced earlier in the same 
build (e.g. by
+            // cpconverter) which do not exist when the lifecycle participant 
runs.
+            for (final Feature additional : 
readAdditionalFeatureFiles(aggregate.additionalFeatureFiles)) {
+                selection.put(additional.getId().toMvnId(), additional);
+            }
+
             if (selection.isEmpty()) {
                 getLog().warn("No features found for aggregate with classifier 
" + aggregate.classifier);
             }
@@ -146,6 +160,10 @@ public class AggregateFeaturesMojo extends 
AbstractIncludingFeatureMojo {
                                     false)
                             .toArray(MergeHandler[]::new))
                     
.addPostProcessExtensions(postProcessHandlers().toArray(PostProcessHandler[]::new));
+            if (aggregate.osgiBsnCollisionDetection) {
+                builderContext.setOsgiBsnCollisionDetection(true);
+            }
+
             for (final ArtifactId rule : aggregate.getArtifactOverrideRules()) 
{
                 builderContext.addArtifactsOverride(rule);
             }
@@ -216,6 +234,25 @@ public class AggregateFeaturesMojo extends 
AbstractIncludingFeatureMojo {
         return Stream.concat(serviceLoaderHandlers, additionalHandlers);
     }
 
+    private List<Feature> readAdditionalFeatureFiles(final List<File> files) 
throws MojoExecutionException {
+        final List<Feature> result = new ArrayList<>();
+        if (files == null || files.isEmpty()) {
+            return result;
+        }
+        for (final File file : files) {
+            if (!file.isFile()) {
+                throw new MojoExecutionException("additionalFeatureFiles entry 
not found: " + file.getAbsolutePath());
+            }
+            try (final Reader reader = Files.newBufferedReader(file.toPath(), 
StandardCharsets.UTF_8)) {
+                result.add(FeatureJSONReader.read(reader, 
file.getAbsolutePath()));
+            } catch (final IOException e) {
+                throw new MojoExecutionException(
+                        "Unable to read additional feature file " + 
file.getAbsolutePath() + " : " + e.getMessage(), e);
+            }
+        }
+        return result;
+    }
+
     Feature assembleFeature(
             final ArtifactId newFeatureID, final BuilderContext 
builderContext, final Map<String, Feature> selection)
             throws MojoExecutionException {
diff --git 
a/src/test/java/org/apache/sling/feature/maven/mojos/AggregateFeaturesMojoTest.java
 
b/src/test/java/org/apache/sling/feature/maven/mojos/AggregateFeaturesMojoTest.java
index ef663b0..c254e3e 100644
--- 
a/src/test/java/org/apache/sling/feature/maven/mojos/AggregateFeaturesMojoTest.java
+++ 
b/src/test/java/org/apache/sling/feature/maven/mojos/AggregateFeaturesMojoTest.java
@@ -837,6 +837,64 @@ public class AggregateFeaturesMojoTest {
         }
     }
 
+    @Test
+    public void testAdditionalFeatureFilesAreMerged() throws Exception {
+        TestContext ctx = prepareTestContext("/aggregate-features/dir2", new 
String[] {"test_w.json"});
+
+        File extra = folder.newFile("extra-feature.json");
+        java.nio.file.Files.write(
+                extra.toPath(),
+                ("{\"id\":\"org.test:extra:1.0.0:slingosgifeature\","
+                                + 
"\"bundles\":[\"org.apache.sling:extrabundle:2\"]}")
+                        .getBytes(java.nio.charset.StandardCharsets.UTF_8));
+
+        ctx.getMojo().aggregates.get(0).additionalFeatureFiles = 
Collections.singletonList(extra);
+        ctx.getMojo().execute();
+
+        Feature genFeat = ctx.getFeatureMap().get(":aggregate:aggregated:T");
+        assertNotNull(genFeat);
+        Set<ArtifactId> actualBundles = new HashSet<>();
+        for (org.apache.sling.feature.Artifact art : genFeat.getBundles()) {
+            actualBundles.add(art.getId());
+        }
+        assertTrue(
+                "bundle from dir2/test_w.json should be present",
+                actualBundles.contains(new ArtifactId("org.apache.sling", 
"someotherbundle", "1", null, null)));
+        assertTrue(
+                "bundle from additionalFeatureFiles should be present",
+                actualBundles.contains(new ArtifactId("org.apache.sling", 
"extrabundle", "2", null, null)));
+    }
+
+    @Test
+    public void testAdditionalFeatureFilesMissingFails() throws Exception {
+        TestContext ctx = prepareTestContext("/aggregate-features/dir2", new 
String[] {"test_w.json"});
+
+        File missing = new File(folder.getRoot(), "does-not-exist.json");
+        ctx.getMojo().aggregates.get(0).additionalFeatureFiles = 
Collections.singletonList(missing);
+
+        try {
+            ctx.getMojo().execute();
+            fail("expected MojoExecutionException for missing 
additionalFeatureFiles entry");
+        } catch (MojoExecutionException e) {
+            assertTrue(
+                    "message should reference the missing file", 
e.getMessage().contains("does-not-exist.json"));
+        }
+    }
+
+    @Test
+    public void testOsgiBsnCollisionDetectionFlagPropagates() throws Exception 
{
+        TestContext ctx = prepareTestContext("/aggregate-features/dir2", new 
String[] {"test_w.json"});
+
+        ctx.getMojo().aggregates.get(0).osgiBsnCollisionDetection = true;
+
+        // The assembly must succeed when there are no BSN collisions even 
with detection on.
+        // (Negative case — collision raises an exception — is covered in the 
feature-core unit tests
+        // for OsgiBsnDeduplicator; aggregating live OSGi bundles here would 
require a heavier fixture.)
+        ctx.getMojo().execute();
+        Feature genFeat = ctx.getFeatureMap().get(":aggregate:aggregated:T");
+        assertNotNull(genFeat);
+    }
+
     @Test
     public void customAggregate() throws Exception {
 

Reply via email to