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

alien11689 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/aries.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 102082244 ARIES-2241: Fix fragment ServiceLoader capability merging 
(#878)
102082244 is described below

commit 1020822442210185179028c196a53853f061bc9c
Author: Václav Haisman <[email protected]>
AuthorDate: Fri Aug 7 07:04:56 2026 +0200

    ARIES-2241: Fix fragment ServiceLoader capability merging (#878)
    
    Keep fragment contributions when the host declares the same capability 
namespace.
---
 .../spifly/ProviderBundleTrackerCustomizer.java    |  19 +--
 ...ndleTrackerCustomizerGenericCapabilityTest.java | 134 +++++++++++++++------
 spi-fly/spi-fly-examples/pom.xml                   |   2 +
 .../spi-fly-example-provider5-bundle/pom.xml       |  78 ++++++++++++
 .../aries/spifly/mysvc/impl5/SPIProviderImpl5.java |  28 +++++
 .../org.apache.aries.spifly.mysvc.SPIProvider      |   1 +
 .../spi-fly-example-provider5-fragment/pom.xml     |  79 ++++++++++++
 .../spifly/mysvc/impl5frag/SPIProvider2Impl.java   |  28 +++++
 .../org.apache.aries.spifly.mysvc.SPIProvider2     |   1 +
 .../apache/aries/spifly/mysvc/SPIProvider2.java    |  29 +++++
 .../apache/aries/spifly/itests/InitialTest.java    |  44 +++++++
 11 files changed, 399 insertions(+), 44 deletions(-)

diff --git 
a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java
 
b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java
index 974daec81..1dc7e6bef 100644
--- 
a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java
+++ 
b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java
@@ -297,8 +297,9 @@ public class ProviderBundleTrackerCustomizer implements 
BundleTrackerCustomizer
     }
 
     private String getHeaderFromBundleOrFragment(Bundle bundle, String 
headerName, String matchString) {
+        final boolean mergeHeader = MERGE_HEADERS.contains(headerName);
         Parameters headerParameters = new 
Parameters(bundle.getHeaders().get(headerName));
-        if (matches(headerParameters.toString(), matchString) && 
!MERGE_HEADERS.contains(headerName)) {
+        if (matches(headerParameters.toString(), matchString) && !mergeHeader) 
{
             return headerParameters.isEmpty() ? null : 
headerParameters.toString();
         }
 
@@ -309,15 +310,19 @@ public class ProviderBundleTrackerCustomizer implements 
BundleTrackerCustomizer
                 for (BundleWire wire : 
wiring.getProvidedWires("osgi.wiring.host")) {
                     Bundle fragment = 
wire.getRequirement().getRevision().getBundle();
                     Parameters fragmentParameters = new 
Parameters(fragment.getHeaders().get(headerName));
-                    if (MERGE_HEADERS.contains(headerName)) {
-                        headerParameters.mergeWith(fragmentParameters, false);
+                    if (mergeHeader) {
+                        // Parameters.mergeWith merges the attributes of 
colliding map entries.
+                        // Fragment headers are parsed independently, so 
append every clause and
+                        // let Parameters.add assign fresh duplicate markers 
where needed.
+                        for (Entry<String, Attrs> entry : 
fragmentParameters.entrySet()) {
+                            headerParameters.add(entry.getKey(), new 
Attrs(entry.getValue()));
+                        }
                     }
                     else {
                         headerParameters = fragmentParameters;
-                    }
-
-                    if (matches(headerParameters.toString(), matchString)) {
-                        return headerParameters.toString();
+                        if (matches(headerParameters.toString(), matchString)) 
{
+                            return headerParameters.toString();
+                        }
                     }
                 }
             }
diff --git 
a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java
 
b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java
index 217f28d9e..4cf5ddbff 100644
--- 
a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java
+++ 
b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java
@@ -108,7 +108,7 @@ public class 
ProviderBundleTrackerCustomizerGenericCapabilityTest {
     }
 
     @Test
-    public void testCapReqHeadersInFragment() throws Exception {
+    public void testCapReqHeadersInFragmentWhenHostAlreadyProvidesCapability() 
throws Exception {
         Bundle mediatorBundle = EasyMock.createMock(Bundle.class);
         
EasyMock.expect(mediatorBundle.getBundleId()).andReturn(42l).anyTimes();
         EasyMock.replay(mediatorBundle);
@@ -119,49 +119,62 @@ public class 
ProviderBundleTrackerCustomizerGenericCapabilityTest {
 
         ProviderBundleTrackerCustomizer customizer = new 
ProviderBundleTrackerCustomizer(activator, mediatorBundle);
 
-        ServiceRegistration<?> sreg = 
EasyMock.createNiceMock(ServiceRegistration.class);
-        EasyMock.replay(sreg);
-
-        BundleContext implBC = mockSPIBundleContext(sreg);
+        BundleContext implBC = mockSPIBundleContext4();
         Dictionary<String, String> headers = new Hashtable<String, String>();
-        // A typical requirement that is not for us...
-        headers.put(SpiFlyConstants.REQUIRE_CAPABILITY, 
"osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.6))\"");
-
-        List<BundleWire> wires = new ArrayList<BundleWire>();
-        BundleWire wire = EasyMock.createMock(BundleWire.class);
-        Bundle fragment = EasyMock.createMock(Bundle.class);
-        BundleRevision frev = EasyMock.createMock(BundleRevision.class);
-        EasyMock.expect(frev.getBundle()).andReturn(fragment).anyTimes();
-        EasyMock.replay(frev);
-        BundleRequirement req = EasyMock.createMock(BundleRequirement.class);
-        EasyMock.expect(req.getRevision()).andReturn(frev).anyTimes();
-        EasyMock.replay(req);
-        EasyMock.expect(wire.getRequirement()).andReturn(req).anyTimes();
-        EasyMock.replay(wire);
-        wires.add(wire);
-
-        BundleWiring bw = EasyMock.createMock(BundleWiring.class);
-        
EasyMock.expect(bw.getProvidedWires("osgi.wiring.host")).andReturn(wires).anyTimes();
-        EasyMock.replay(bw);
-
-        BundleRevision rev = EasyMock.createMock(BundleRevision.class);
-        EasyMock.expect(rev.getWiring()).andReturn(bw).anyTimes();
-        EasyMock.expect(rev.getTypes()).andReturn(0).anyTimes();
-        EasyMock.replay(rev);
-        Bundle implBundle = mockSPIBundle(implBC, headers, rev);
+        headers.put(SpiFlyConstants.REQUIRE_CAPABILITY, 
SpiFlyConstants.PROVIDER_REQUIREMENT);
+        headers.put(SpiFlyConstants.PROVIDE_CAPABILITY, 
SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "; " +
+                SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + 
"=org.apache.aries.mytest.MySPI");
 
         Dictionary<String, String> fheaders = new Hashtable<String, String>();
         fheaders.put(SpiFlyConstants.REQUIRE_CAPABILITY, 
SpiFlyConstants.PROVIDER_REQUIREMENT);
         fheaders.put(SpiFlyConstants.PROVIDE_CAPABILITY, 
SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "; " +
-              SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + 
"=org.apache.aries.mytest.MySPI");
-        EasyMock.expect(fragment.getHeaders()).andReturn(fheaders).anyTimes();
-        EasyMock.replay(fragment);
+              SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + 
"=org.apache.aries.mytest.MySPI2");
+        Bundle fragment = mockFragment(fheaders);
+        Bundle implBundle = mockSPIBundle4(implBC, headers, 
mockHostRevision(fragment));
 
         assertEquals("Precondition", 0, 
activator.findProviderBundles("org.apache.aries.mytest.MySPI").size());
-        customizer.addingBundle(implBundle, null);
-        Collection<Bundle> bundles = 
activator.findProviderBundles("org.apache.aries.mytest.MySPI");
-        assertEquals(1, bundles.size());
-        assertSame(implBundle, bundles.iterator().next());
+        assertEquals("Precondition", 0, 
activator.findProviderBundles("org.apache.aries.mytest.MySPI2").size());
+        @SuppressWarnings("rawtypes")
+        List<ServiceRegistration> registrations = 
customizer.addingBundle(implBundle, null);
+        assertEquals(3, registrations.size());
+        assertProviderBundle(activator, "org.apache.aries.mytest.MySPI", 
implBundle);
+        assertProviderBundle(activator, "org.apache.aries.mytest.MySPI2", 
implBundle);
+    }
+
+    @Test
+    public void testCapReqHeadersInMultipleFragments() throws Exception {
+        Bundle mediatorBundle = EasyMock.createMock(Bundle.class);
+        
EasyMock.expect(mediatorBundle.getBundleId()).andReturn(42l).anyTimes();
+        EasyMock.replay(mediatorBundle);
+        BaseActivator activator = new BaseActivator() {
+            @Override
+            public void start(BundleContext context) throws Exception {}
+        };
+
+        ProviderBundleTrackerCustomizer customizer = new 
ProviderBundleTrackerCustomizer(activator, mediatorBundle);
+        BundleContext implBC = mockSPIBundleContext4();
+
+        Dictionary<String, String> headers = new Hashtable<String, String>();
+        headers.put(SpiFlyConstants.REQUIRE_CAPABILITY, 
"osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.6))\"");
+
+        Dictionary<String, String> fheaders1 = new Hashtable<String, String>();
+        fheaders1.put(SpiFlyConstants.REQUIRE_CAPABILITY, 
SpiFlyConstants.PROVIDER_REQUIREMENT);
+        fheaders1.put(SpiFlyConstants.PROVIDE_CAPABILITY, 
SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "; " +
+                SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + 
"=org.apache.aries.mytest.MySPI");
+
+        Dictionary<String, String> fheaders2 = new Hashtable<String, String>();
+        fheaders2.put(SpiFlyConstants.REQUIRE_CAPABILITY, 
SpiFlyConstants.PROVIDER_REQUIREMENT);
+        fheaders2.put(SpiFlyConstants.PROVIDE_CAPABILITY, 
SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "; " +
+                SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + 
"=org.apache.aries.mytest.MySPI2");
+
+        Bundle implBundle = mockSPIBundle4(implBC, headers,
+                mockHostRevision(mockFragment(fheaders1), 
mockFragment(fheaders2)));
+
+        @SuppressWarnings("rawtypes")
+        List<ServiceRegistration> registrations = 
customizer.addingBundle(implBundle, null);
+        assertEquals(3, registrations.size());
+        assertProviderBundle(activator, "org.apache.aries.mytest.MySPI", 
implBundle);
+        assertProviderBundle(activator, "org.apache.aries.mytest.MySPI2", 
implBundle);
     }
 
     @Test
@@ -662,9 +675,15 @@ public class 
ProviderBundleTrackerCustomizerGenericCapabilityTest {
     }
 
     private Bundle mockSPIBundle4(BundleContext implBC, Dictionary<String, 
String> headers) throws ClassNotFoundException {
+        return mockSPIBundle4(implBC, headers, null);
+    }
+
+    private Bundle mockSPIBundle4(BundleContext implBC, Dictionary<String, 
String> headers, BundleRevision rev) throws ClassNotFoundException {
         Bundle implBundle = EasyMock.createNiceMock(Bundle.class);
         
EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes();
         EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes();
+        if (rev != null)
+            
EasyMock.expect(implBundle.adapt(BundleRevision.class)).andReturn(rev).anyTimes();
 
         // List the resources found at META-INF/services in the test bundle
         URL dir = getClass().getResource("impl4/META-INF/services");
@@ -690,6 +709,47 @@ public class 
ProviderBundleTrackerCustomizerGenericCapabilityTest {
         return implBundle;
     }
 
+    private Bundle mockFragment(Dictionary<String, String> headers) {
+        Bundle fragment = EasyMock.createMock(Bundle.class);
+        EasyMock.expect(fragment.getHeaders()).andReturn(headers).anyTimes();
+        EasyMock.replay(fragment);
+        return fragment;
+    }
+
+    private BundleRevision mockHostRevision(Bundle... fragments) {
+        List<BundleWire> wires = new ArrayList<BundleWire>();
+        for (Bundle fragment : fragments) {
+            BundleRevision fragmentRevision = 
EasyMock.createMock(BundleRevision.class);
+            
EasyMock.expect(fragmentRevision.getBundle()).andReturn(fragment).anyTimes();
+            EasyMock.replay(fragmentRevision);
+
+            BundleRequirement requirement = 
EasyMock.createMock(BundleRequirement.class);
+            
EasyMock.expect(requirement.getRevision()).andReturn(fragmentRevision).anyTimes();
+            EasyMock.replay(requirement);
+
+            BundleWire wire = EasyMock.createMock(BundleWire.class);
+            
EasyMock.expect(wire.getRequirement()).andReturn(requirement).anyTimes();
+            EasyMock.replay(wire);
+            wires.add(wire);
+        }
+
+        BundleWiring wiring = EasyMock.createMock(BundleWiring.class);
+        
EasyMock.expect(wiring.getProvidedWires("osgi.wiring.host")).andReturn(wires).anyTimes();
+        EasyMock.replay(wiring);
+
+        BundleRevision revision = EasyMock.createMock(BundleRevision.class);
+        EasyMock.expect(revision.getWiring()).andReturn(wiring).anyTimes();
+        EasyMock.expect(revision.getTypes()).andReturn(0).anyTimes();
+        EasyMock.replay(revision);
+        return revision;
+    }
+
+    private void assertProviderBundle(BaseActivator activator, String 
serviceName, Bundle implBundle) {
+        Collection<Bundle> bundles = 
activator.findProviderBundles(serviceName);
+        assertEquals(1, bundles.size());
+        assertSame(implBundle, bundles.iterator().next());
+    }
+
     @SuppressWarnings("rawtypes")
     private static class ServiceRegistrationImpl implements 
ServiceRegistration<Object>, ServiceReference {
         private final Object serviceObject;
diff --git a/spi-fly/spi-fly-examples/pom.xml b/spi-fly/spi-fly-examples/pom.xml
index 048b78d0a..12dc2bfc1 100644
--- a/spi-fly/spi-fly-examples/pom.xml
+++ b/spi-fly/spi-fly-examples/pom.xml
@@ -58,6 +58,8 @@
         <module>spi-fly-example-provider2-bundle</module>
         <module>spi-fly-example-provider3-bundle</module>
         <module>spi-fly-example-provider3-fragment</module>
+        <module>spi-fly-example-provider5-bundle</module>
+        <module>spi-fly-example-provider5-fragment</module>
         <module>spi-fly-example-client1-jar</module>
         <module>spi-fly-example-client1-bundle</module>
         <module>spi-fly-example-client2-bundle</module>
diff --git a/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/pom.xml 
b/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/pom.xml
new file mode 100644
index 000000000..9d7102b1f
--- /dev/null
+++ b/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/pom.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+    <!--
+ 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.
+    -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.aries.spifly.examples</groupId>
+        <artifactId>spi-fly-examples</artifactId>
+        <version>1.0.6-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>org.apache.aries.spifly.examples.provider5.bundle</artifactId>
+    <packaging>bundle</packaging>
+    <name>Apache Aries Example SPI Provider Bundle 5</name>
+    <description>
+        A bundle that natively declares its own osgi.serviceloader 
Provide-Capability for
+        SPIProvider (mimicking a real-world host such as 
ch.qos.logback.classic, which natively
+        provides more than one osgi.serviceloader capability of its own). Used 
together with
+        spi-fly-example-provider5-fragment to reproduce a bug where a 
fragment's own
+        osgi.serviceloader capability (for a different service, SPIProvider2) 
gets silently
+        dropped when merged into a host that already has one.
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.aries.spifly.examples</groupId>
+            
<artifactId>org.apache.aries.spifly.examples.spi.bundle</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <properties>
+        <aries.osgi.export.pkg />
+        <lastReleaseVersion>1.0.1-SNAPSHOT</lastReleaseVersion>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        <Import-Package>
+                            *
+                        </Import-Package>
+                        <Export-Package>
+                        </Export-Package>
+                        <Private-Package>
+                            org.apache.aries.spifly.mysvc.impl5
+                        </Private-Package>
+                        <Require-Capability>osgi.extender; 
filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
+                        <Provide-Capability>osgi.serviceloader; 
osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider</Provide-Capability>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git 
a/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/src/main/java/org/apache/aries/spifly/mysvc/impl5/SPIProviderImpl5.java
 
b/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/src/main/java/org/apache/aries/spifly/mysvc/impl5/SPIProviderImpl5.java
new file mode 100644
index 000000000..a9cff5e53
--- /dev/null
+++ 
b/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/src/main/java/org/apache/aries/spifly/mysvc/impl5/SPIProviderImpl5.java
@@ -0,0 +1,28 @@
+/**
+ * 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.aries.spifly.mysvc.impl5;
+
+import org.apache.aries.spifly.mysvc.SPIProvider;
+
+public class SPIProviderImpl5 extends SPIProvider {
+       @Override
+       public String doit() {
+               return "Doing it from provider5 host!";
+       }
+}
diff --git 
a/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/src/main/resources/META-INF/services/org.apache.aries.spifly.mysvc.SPIProvider
 
b/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/src/main/resources/META-INF/services/org.apache.aries.spifly.mysvc.SPIProvider
new file mode 100644
index 000000000..258c84d0a
--- /dev/null
+++ 
b/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/src/main/resources/META-INF/services/org.apache.aries.spifly.mysvc.SPIProvider
@@ -0,0 +1 @@
+org.apache.aries.spifly.mysvc.impl5.SPIProviderImpl5
diff --git 
a/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/pom.xml 
b/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/pom.xml
new file mode 100644
index 000000000..6f55f72d9
--- /dev/null
+++ b/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/pom.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+    <!--
+ 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.
+    -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.aries.spifly.examples</groupId>
+        <artifactId>spi-fly-examples</artifactId>
+        <version>1.0.6-SNAPSHOT</version>
+    </parent>
+
+    
<artifactId>org.apache.aries.spifly.examples.provider5.fragment</artifactId>
+    <packaging>bundle</packaging>
+    <name>Apache Aries Example SPI Provider Bundle 5 Fragment</name>
+    <description>
+        A fragment attached to spi-fly-example-provider5-bundle (a host that 
already natively
+        provides one osgi.serviceloader capability of its own) contributing a 
second, distinct
+        osgi.serviceloader capability for SPIProvider2. Reproduces a bug where 
the fragment's
+        capability is silently dropped by ProviderBundleTrackerCustomizer's 
header-merging logic
+        because it collides, at the bnd Parameters map-key level, with the 
host's own pre-existing
+        osgi.serviceloader clause.
+    </description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.aries.spifly.examples</groupId>
+            
<artifactId>org.apache.aries.spifly.examples.spi.bundle</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <properties>
+        <aries.osgi.export.pkg />
+        <lastReleaseVersion>1.0.1-SNAPSHOT</lastReleaseVersion>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <configuration>
+                    <instructions>
+                        
<Fragment-Host>org.apache.aries.spifly.examples.provider5.bundle</Fragment-Host>
+                        <Import-Package>
+                            *
+                        </Import-Package>
+                        <Export-Package>
+                        </Export-Package>
+                        <Private-Package>
+                            org.apache.aries.spifly.mysvc.impl5frag
+                        </Private-Package>
+                        <Require-Capability>osgi.extender; 
filter:="(osgi.extender=osgi.serviceloader.registrar)"</Require-Capability>
+                        <Provide-Capability>osgi.serviceloader; 
osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider2</Provide-Capability>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git 
a/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/src/main/java/org/apache/aries/spifly/mysvc/impl5frag/SPIProvider2Impl.java
 
b/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/src/main/java/org/apache/aries/spifly/mysvc/impl5frag/SPIProvider2Impl.java
new file mode 100644
index 000000000..da54222b5
--- /dev/null
+++ 
b/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/src/main/java/org/apache/aries/spifly/mysvc/impl5frag/SPIProvider2Impl.java
@@ -0,0 +1,28 @@
+/**
+ * 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.aries.spifly.mysvc.impl5frag;
+
+import org.apache.aries.spifly.mysvc.SPIProvider2;
+
+public class SPIProvider2Impl extends SPIProvider2 {
+       @Override
+       public String doit() {
+               return "Doing it from provider5 fragment!";
+       }
+}
diff --git 
a/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/src/main/resources/META-INF/services/org.apache.aries.spifly.mysvc.SPIProvider2
 
b/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/src/main/resources/META-INF/services/org.apache.aries.spifly.mysvc.SPIProvider2
new file mode 100644
index 000000000..47c8e7362
--- /dev/null
+++ 
b/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/src/main/resources/META-INF/services/org.apache.aries.spifly.mysvc.SPIProvider2
@@ -0,0 +1 @@
+org.apache.aries.spifly.mysvc.impl5frag.SPIProvider2Impl
diff --git 
a/spi-fly/spi-fly-examples/spi-fly-example-spi-bundle/src/main/java/org/apache/aries/spifly/mysvc/SPIProvider2.java
 
b/spi-fly/spi-fly-examples/spi-fly-example-spi-bundle/src/main/java/org/apache/aries/spifly/mysvc/SPIProvider2.java
new file mode 100644
index 000000000..e5e409702
--- /dev/null
+++ 
b/spi-fly/spi-fly-examples/spi-fly-example-spi-bundle/src/main/java/org/apache/aries/spifly/mysvc/SPIProvider2.java
@@ -0,0 +1,29 @@
+/**
+ * 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.aries.spifly.mysvc;
+
+/**
+ * A second, distinct SPI type, used alongside {@link SPIProvider} to 
reproduce a bug where a
+ * host bundle that already natively provides one {@code osgi.serviceloader} 
capability causes an
+ * attached fragment's own {@code osgi.serviceloader} capability (for this 
interface) to be
+ * silently dropped during header merging in {@code 
ProviderBundleTrackerCustomizer}.
+ */
+public abstract class SPIProvider2 {
+       public abstract String doit();
+}
diff --git 
a/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java
 
b/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java
index 8f1da37c3..386fd6eb9 100644
--- 
a/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java
+++ 
b/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java
@@ -122,6 +122,50 @@ public class InitialTest {
         ).doesNotContain("Doing it!", "Doing it too!");
     }
 
+    /**
+     * Regression test for {@code ProviderBundleTrackerCustomizer}'s header 
merging. Without a
+     * clause-preserving merge, a fragment's own {@code osgi.serviceloader} 
Provide-Capability
+     * clause (for {@code SPIProvider2}) is silently dropped when merged into 
a host bundle
+     * (provider5-bundle) that already natively declares one {@code 
osgi.serviceloader} clause of
+     * its own (for {@code SPIProvider}) -
+     * mirroring a real-world host such as {@code ch.qos.logback.classic}, 
which itself natively
+     * provides more than one {@code osgi.serviceloader} capability. The 
collision happens because
+     * {@code aQute.bnd.header.Parameters} is a {@code Map<clauseName, 
Attrs>}: merging two
+     * independently-parsed headers that both use the unmarked clause name 
{@code osgi.serviceloader}
+     * falls into {@code Attrs.mergeWith(..., overwrite=false)} instead of 
being added as a third,
+     * distinct clause - so the fragment's {@code 
osgi.serviceloader=SPIProvider2} attribute value
+     * is discarded in favour of the host's own pre-existing one.
+     *
+     * <p>The first assertion verifies the host's own capability. The second 
is the regression
+     * assertion: the merge must keep same-named capability clauses from 
different sources
+     * distinct so that the fragment's capability is registered alongside the 
host's.
+     *
+     * @throws Exception if the example bundles cannot be installed or 
inspected
+     */
+    @Test
+    public void example5() throws Exception {
+        Bundle provider5fragment = 
assertBundleInstallation(getExampleJar("spi-fly-example-provider5-fragment"), 
true);
+        Bundle provider5Bundle = 
assertBundleInstallation(getExampleJar("spi-fly-example-provider5-bundle"));
+        
BundleAssert.assertThat(provider5fragment).isFragment().isInState(Bundle.RESOLVED);
+        assertFragmentAttached(provider5Bundle, provider5fragment);
+
+        assertThat(
+            
bundleContext.getServiceReferences("org.apache.aries.spifly.mysvc.SPIProvider", 
null)
+        ).as(
+            "the host bundle's own native osgi.serviceloader capability should 
always be registered"
+        ).isNotEmpty();
+
+        assertThat(
+            
bundleContext.getServiceReferences("org.apache.aries.spifly.mysvc.SPIProvider2",
 null)
+        ).as(
+            "the fragment's osgi.serviceloader capability for a second, 
distinct service should " +
+            "ALSO be registered alongside the host's own - if this is empty, " 
+
+            "ProviderBundleTrackerCustomizer silently dropped it while merging 
the fragment's " +
+            "Provide-Capability header into the host's, because the host 
already had one " +
+            "osgi.serviceloader clause of its own"
+        ).isNotEmpty();
+    }
+
     @Test
     public void example4() throws Exception {
         
assertBundleInstallation(getExampleJar("spi-fly-example-resource-provider-bundle"));

Reply via email to