Merge pull request #11 from bosschaert/SLING-9501
SLING-9501 Packages provided by a non-api-region enabled feature should be on par with the global region
diff --git a/src/main/java/org/apache/sling/feature/apiregions/impl/ResolverHookImpl.java b/src/main/java/org/apache/sling/feature/apiregions/impl/ResolverHookImpl.java
index d6a1881..fe5d634 100644
--- a/src/main/java/org/apache/sling/feature/apiregions/impl/ResolverHookImpl.java
+++ b/src/main/java/org/apache/sling/feature/apiregions/impl/ResolverHookImpl.java
@@ -63,21 +63,35 @@
if (!PackageNamespace.PACKAGE_NAMESPACE.equals(requirement.getNamespace()))
return;
+ if (candidates.isEmpty())
+ return;
+
+ Object pkg = candidates.iterator().next().getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE);
+ if (!(pkg instanceof String)) {
+ return;
+ }
+ String packageName = (String) pkg;
+
Bundle reqBundle = requirement.getRevision().getBundle();
long reqBundleID = reqBundle.getBundleId();
- Set<String> reqRegions = new HashSet<>(this.configuration.getDefaultRegions());
+ Set<String> bareReqRegions = null; // Null means: not opting into API Regions
Set<String> reqFeatures = getFeaturesForBundle(reqBundle);
for (String feature : reqFeatures) {
Set<String> fr = this.configuration.getFeatureRegionMap().get(feature);
if (fr != null) {
- reqRegions.addAll(fr);
+ if (bareReqRegions == null)
+ bareReqRegions = new HashSet<>();
+ bareReqRegions.addAll(fr);
}
}
+ Set<String> reqRegions = new HashSet<>(this.configuration.getDefaultRegions());
+ if (bareReqRegions != null)
+ reqRegions.addAll(bareReqRegions);
Map<BundleCapability, String> coveredCaps = new HashMap<>();
Map<BundleCapability, String> bcFeatureMap = new HashMap<>();
- String packageName = null;
+
nextCapability:
for (BundleCapability bc : candidates) {
BundleRevision rev = bc.getRevision();
@@ -86,14 +100,19 @@
long capBundleID = capBundle.getBundleId();
if (capBundleID == 0) {
// always allow capability from the system bundle
- coveredCaps.put(bc, null); // null value means same bundle, same feature or system bundlee
+ coveredCaps.put(bc, null); // null value means same bundle, same feature or system bundle
continue nextCapability;
}
if (capBundleID == reqBundleID) {
// always allow capability from same bundle
- coveredCaps.put(bc, null); // null value means same bundle, same feature or system bundle
- continue nextCapability;
+
+ // Here we cover the case where the bundle is not in any feature which means that the package is in the 'global' region,
+ // however if the bundle is in a feature then it could be marked as more specific, with a 'null value', which may
+ // happen below.
+ coveredCaps.put(bc, RegionConstants.GLOBAL_REGION);
+
+ // note: don't continue to nextCapability here, this one may be overwritten later...
}
Set<String> capFeatures = getFeaturesForBundle(capBundle);
@@ -106,7 +125,9 @@
for (String capFeat : capFeatures) {
if (reqFeatures.contains(capFeat)) {
// Within a single feature everything can wire to everything else
- coveredCaps.put(bc, null); // null value means same bundle, same feature or system bundle
+
+ // null value means same bundle, same feature or system bundle, but if exported into global region, use 'global' instead
+ coveredCaps.put(bc, isInGlobalRegion(packageName, capFeat) ? RegionConstants.GLOBAL_REGION : null);
continue nextCapability;
}
@@ -121,32 +142,27 @@
List<String> sharedRegions = new ArrayList<>(reqRegions);
sharedRegions.retainAll(capRegions);
- Object pkg = bc.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE);
- if (pkg instanceof String) {
- packageName = (String) pkg;
-
- // Look at specific regions first as they take precedence over the global region
- for (String region : sharedRegions) {
- Set<String> regionPackages = this.configuration.getRegionPackageMap().get(region);
- if (regionPackages != null && regionPackages.contains(packageName)) {
- // If the export is in a region that the feature is also in, then allow
- coveredCaps.put(bc, region);
- continue nextCapability;
- }
- }
-
- // Now check the global region
- Set<String> globalPackages = this.configuration.getRegionPackageMap().get(RegionConstants.GLOBAL_REGION);
- if (globalPackages != null && globalPackages.contains(packageName)) {
- // If the export is in the global region everyone can access
- coveredCaps.put(bc, RegionConstants.GLOBAL_REGION);
+ // Look at specific regions first as they take precedence over the global region
+ for (String region : sharedRegions) {
+ Set<String> regionPackages = this.configuration.getRegionPackageMap().get(region);
+ if (regionPackages != null && regionPackages.contains(packageName)) {
+ // If the export is in a region that the feature is also in, then allow
+ coveredCaps.put(bc, region);
continue nextCapability;
}
}
+
+ // Now check the global region
+ Set<String> globalPackages = this.configuration.getRegionPackageMap().get(RegionConstants.GLOBAL_REGION);
+ if (globalPackages != null && globalPackages.contains(packageName)) {
+ // If the export is in the global region everyone can access
+ coveredCaps.put(bc, RegionConstants.GLOBAL_REGION);
+ continue nextCapability;
+ }
}
}
- pruneCoveredCaps(reqRegions, coveredCaps);
+ pruneCoveredCaps(bareReqRegions, coveredCaps);
List<BundleCapability> removedCandidates = new ArrayList<>(candidates);
// Remove any capabilities that are not covered
@@ -184,15 +200,41 @@
}
}
- /*
+ /**
+ * Check if the package is exported in the global region
+ * @param packageName The package
+ * @param capFeat The feature where it is found
+ * @return If the feature exports to the global region and the package is exported into the global region
+ */
+ private boolean isInGlobalRegion(String packageName, String capFeat) {
+ Set<String> capRegions = this.configuration.getFeatureRegionMap().get(capFeat);
+ if (capRegions != null && capRegions.contains(RegionConstants.GLOBAL_REGION)) {
+ Set<String> globalPackages = this.configuration.getRegionPackageMap().get(RegionConstants.GLOBAL_REGION);
+ if (globalPackages.contains(packageName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
* If there are multiple choices of capabilities and some of the capabilities are in the global
- * region while others are in another named region, take out the capabilities from the global
- * region so that the requirement gets wired to the more 'specifc' one than the global one.
- * Capabilities from bundle 0 (the system bundle), the same bundle as the requirer and from the
- * same feature as the requirer should always be kept. These are marked in the capMap with a
- * {@code null} region value.
+ * region while others are in another named region or in a feature-private region, take out the
+ * capabilities from the global region so that the requirement gets wired to the more 'specific'
+ * one than the global one.
+ * Capabilities that are considered specific but not in a named region are marked in the CapMap
+ * with a {@code null} region value.
+ *
+ * @param reqRegions The regions declared in the requiring feature. If {@code null} is passed in
+ * the requirement did not opt into the API Regions
+ * @param capMap The map of capabilities to regions where they are found in
*/
private void pruneCoveredCaps(Set<String> reqRegions, Map<BundleCapability,String> capMap) {
+ if (reqRegions == null) {
+ // No regions (other than global) for the requirement: do nothing
+ return;
+ }
+
Set<String> reqNonGlobalRegions = new HashSet<>(reqRegions);
reqNonGlobalRegions.remove(RegionConstants.GLOBAL_REGION);
@@ -201,12 +243,6 @@
return;
}
- if (reqRegions.size() == 0
- || Collections.singleton(RegionConstants.GLOBAL_REGION).equals(reqRegions)) {
- // No regions (other than global) for the requirement: do nothing
- return;
- }
-
List<BundleCapability> specificCaps = new ArrayList<>();
for (Iterator<Map.Entry<BundleCapability,String>> it = capMap.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<BundleCapability,String> entry = it.next();
@@ -236,7 +272,7 @@
BundleCapability cap = it.next();
if (!specificCaps.contains(cap)) {
it.remove();
- Activator.LOG.log(Level.INFO, "Removing candidate {0} which is in region {1} as more specific candidates are available in regions {2}",
+ Activator.LOG.log(Level.INFO, "Removing candidate {0} which is in region {1} as more specific candidate(s) are available in regions {2}",
new Object[] {
cap, capMap.get(cap),
specificCaps.stream().map(c -> "" + c + " region " + capMap.get(c)).collect(Collectors.joining("/"))
diff --git a/src/test/java/org/apache/sling/feature/apiregions/impl/RegionConfigurationTest.java b/src/test/java/org/apache/sling/feature/apiregions/impl/RegionConfigurationTest.java
index e9b0468..714eba9 100644
--- a/src/test/java/org/apache/sling/feature/apiregions/impl/RegionConfigurationTest.java
+++ b/src/test/java/org/apache/sling/feature/apiregions/impl/RegionConfigurationTest.java
@@ -443,8 +443,8 @@
ResolverHookImpl rhi = new ResolverHookImpl(cfg);
BundleRequirement req = mockRequirement("b4", new Version(9,9,9,"something"), ctx);
- BundleCapability cap1 = mockCapability("b1", new Version(1,0,0), ctx);
- BundleCapability cap2 = mockCapability("b2", new Version(1,2,3), ctx);
+ BundleCapability cap1 = mockCapability("org.foo.bar", "b1", new Version(1,0,0), ctx);
+ BundleCapability cap2 = mockCapability("org.foo.bar", "b2", new Version(1,2,3), ctx);
ArrayList<BundleCapability> caps1 = new ArrayList<>(Arrays.asList(cap1, cap2));
rhi.filterMatches(req, caps1);
@@ -502,13 +502,17 @@
return req;
}
- private BundleCapability mockCapability(String bsn, Version bver, BundleContext mockContext) {
+ private BundleCapability mockCapability(String pkg, String bsn, Version bver, BundleContext mockContext) {
+ Map<String, Object> attrs =
+ Collections.<String, Object>singletonMap(PackageNamespace.PACKAGE_NAMESPACE, pkg);
+
BundleRevision br = mockBundleRevision(bsn, bver, mockContext);
- BundleCapability req = Mockito.mock(BundleCapability.class);
- Mockito.when(req.getRevision()).thenReturn(br);
- Mockito.when(req.getNamespace()).thenReturn(PackageNamespace.PACKAGE_NAMESPACE);
- return req;
+ BundleCapability cap = Mockito.mock(BundleCapability.class);
+ Mockito.when(cap.getRevision()).thenReturn(br);
+ Mockito.when(cap.getAttributes()).thenReturn(attrs);
+ Mockito.when(cap.getNamespace()).thenReturn(PackageNamespace.PACKAGE_NAMESPACE);
+ return cap;
}
private BundleRevision mockBundleRevision(String bsn, Version bver, BundleContext mockContext) {
diff --git a/src/test/java/org/apache/sling/feature/apiregions/impl/ResolverHookImplTest.java b/src/test/java/org/apache/sling/feature/apiregions/impl/ResolverHookImplTest.java
index 2525030..bf9d5fb 100644
--- a/src/test/java/org/apache/sling/feature/apiregions/impl/ResolverHookImplTest.java
+++ b/src/test/java/org/apache/sling/feature/apiregions/impl/ResolverHookImplTest.java
@@ -205,6 +205,35 @@
}
@Test
+ public void testOwnBundleInRegionHasPrecedenceOverGlobal() {
+ Map<Entry<String, Version>, List<String>> bsnvermap = new HashMap<>();
+ bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
+ "providing.bundle.infeature", new Version(1,0,0)), Collections.singletonList("b1"));
+ bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
+ "providing.bundle.inglobal", new Version(1,0,0)), Collections.singletonList("b2"));
+
+ Map<String, Set<String>> bfmap = new HashMap<>();
+ bfmap.put("b1", Collections.singleton("f1"));
+
+ Map<String, Set<String>> frmap = new HashMap<>();
+ frmap.put("f1", Collections.emptySet());
+
+ Map<String, Set<String>> rpmap = new HashMap<>();
+
+ ResolverHookImpl rh = new ResolverHookImpl(new RegionConfiguration(bsnvermap, bfmap, frmap, rpmap, Collections.singleton("global")));
+
+ BundleRequirement req1 = mockRequirement("b1", bsnvermap);
+ BundleCapability cap1 = mockCapability("org.foo.bar", "b1", bsnvermap);
+ BundleCapability cap2 = mockCapability("org.foo.bar", "b2", bsnvermap);
+ List<BundleCapability> candidates = new ArrayList<>(Arrays.asList(cap1, cap2));
+ rh.filterMatches(req1, candidates);
+
+ assertEquals("Only the candidate from b1 should be selected, even through b2 is in the global region, "
+ + "because b1 is not exported and as such more specific",
+ Collections.singletonList(cap1), candidates);
+ }
+
+ @Test
public void testOwnFeatureHasPrecendenceOverGlobal() {
Map<Entry<String, Version>, List<String>> bsnvermap = new HashMap<>();
bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
@@ -241,6 +270,44 @@
}
@Test
+ public void testGlobalAndNonAPIRegionsFeatureAreEqual() {
+ Map<Entry<String, Version>, List<String>> bsnvermap = new HashMap<>();
+
+ bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
+ "providing.bundle.infeature", new Version(1,0,0)), Collections.singletonList("b101"));
+ bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
+ "providing.bundle.inglobal", new Version(1,0,0)), Collections.singletonList("b102"));
+ bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
+ "requiring.bundle", new Version(1,0,0)), Collections.singletonList("b99"));
+
+ Map<String, Set<String>> bfmap = new HashMap<>();
+ bfmap.put("b99", Collections.singleton("f1"));
+ bfmap.put("b101", Collections.singleton("f1"));
+
+ Map<String, Set<String>> frmap = new HashMap<>();
+ frmap.put("f1", new HashSet<>(Arrays.asList("r1", "global")));
+
+ Map<String, Set<String>> rpmap = new HashMap<>();
+ rpmap.put("r1", Collections.singleton("org.blah.blah"));
+ rpmap.put(RegionConstants.GLOBAL_REGION, Collections.singleton("org.foo.bar"));
+
+ ResolverHookImpl rh = new ResolverHookImpl(new RegionConfiguration(bsnvermap, bfmap, frmap, rpmap, Collections.singleton("global")));
+
+ // b2 needs to resolve 'org.foo.bar' and there are 2 candidates:
+ // b101 is in the same feature as b2
+ // b102 is in the no feature
+ // In this case both should still be available as both are in the global region
+ BundleRequirement req1 = mockRequirement("b99", bsnvermap);
+ BundleCapability cap1 = mockCapability("org.foo.bar", "b101", bsnvermap);
+ BundleCapability cap2 = mockCapability("org.foo.bar", "b102", bsnvermap);
+ List<BundleCapability> candidates = new ArrayList<>(Arrays.asList(cap1, cap2));
+ Set<BundleCapability> orgCandidates = new HashSet<>(candidates);
+ rh.filterMatches(req1, candidates);
+
+ assertEquals(orgCandidates, new HashSet<>(candidates));
+ }
+
+ @Test
public void testMultipleGlobalOptionsOnly() {
Map<Entry<String, Version>, List<String>> bsnvermap = new HashMap<>();
bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
@@ -661,6 +728,7 @@
Mockito.when(br.getBundle()).thenReturn(bundle);
BundleCapability cap = Mockito.mock(BundleCapability.class);
+ Mockito.when(cap.getNamespace()).thenReturn(PackageNamespace.PACKAGE_NAMESPACE);
Mockito.when(cap.getAttributes()).thenReturn(attrs);
Mockito.when(cap.getRevision()).thenReturn(br);
return cap;