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"));