Merge pull request #2 from bosschaert/SLING-9004

SLING-9004 API Regions runtime should have a configured set of default regions
diff --git a/src/main/java/org/apache/sling/feature/apiregions/impl/Activator.java b/src/main/java/org/apache/sling/feature/apiregions/impl/Activator.java
index 190e29b..27189c3 100644
--- a/src/main/java/org/apache/sling/feature/apiregions/impl/Activator.java
+++ b/src/main/java/org/apache/sling/feature/apiregions/impl/Activator.java
@@ -67,13 +67,14 @@
         if (hookRegistration != null)
             return; // There is already a hook, no need to re-register
 
-        String regions = bundleContext.getProperty(REGIONS_PROPERTY_NAME);
-        if (regions == null)
+        if (bundleContext.getProperty(REGIONS_PROPERTY_NAME) == null) {
+            RegionEnforcer.LOG.log(Level.WARNING, "API Regions not enabled. To enable set framework property: " + REGIONS_PROPERTY_NAME);
             return; // Component not enabled
+        }
 
         Dictionary<String, Object> props = new Hashtable<>();
         try {
-            RegionEnforcer enforcer = new RegionEnforcer(bundleContext, props, regions);
+            RegionEnforcer enforcer = new RegionEnforcer(bundleContext, props);
             hookRegistration = bundleContext.registerService(ResolverHookFactory.class, enforcer, props);
         } catch (Exception e) {
             RegionEnforcer.LOG.log(Level.SEVERE, "Problem activating API Regions runtime enforcement component", e);
diff --git a/src/main/java/org/apache/sling/feature/apiregions/impl/RegionEnforcer.java b/src/main/java/org/apache/sling/feature/apiregions/impl/RegionEnforcer.java
index 5c2310e..d1a1f40 100644
--- a/src/main/java/org/apache/sling/feature/apiregions/impl/RegionEnforcer.java
+++ b/src/main/java/org/apache/sling/feature/apiregions/impl/RegionEnforcer.java
@@ -50,12 +50,13 @@
 
     static final String CLASSLOADER_PSEUDO_PROTOCOL = "classloader://";
     static final String APIREGIONS_JOINGLOBAL = "sling.feature.apiregions.joinglobal";
+    static final String DEFAULT_REGIONS = "sling.feature.apiregions.default";
     static final String PROPERTIES_RESOURCE_PREFIX = "sling.feature.apiregions.resource.";
     static final String PROPERTIES_FILE_LOCATION = "sling.feature.apiregions.location";
 
     static final String IDBSNVER_FILENAME = "idbsnver.properties";
-    static final String BUNDLE_FEATURE_FILENAME = "bundleOrigins.properties";
-    static final String FEATURE_REGION_FILENAME = "regionOrigins.properties";
+    static final String BUNDLE_FEATURE_FILENAME = "bundles.properties";
+    static final String FEATURE_REGION_FILENAME = "features.properties";
     static final String REGION_PACKAGE_FILENAME = "regions.properties";
 
     static final Logger LOG = Logger.getLogger(ResolverHookImpl.class.getName());
@@ -64,9 +65,9 @@
     final Map<String, Set<String>> bundleFeatureMap;
     final Map<String, Set<String>> featureRegionMap;
     final Map<String, Set<String>> regionPackageMap;
-    final Set<String> enabledRegions;
+    final Set<String> defaultRegions;
 
-    RegionEnforcer(BundleContext context, Dictionary<String, Object> regProps, String regionsProp)
+    RegionEnforcer(BundleContext context, Dictionary<String, Object> regProps)
             throws IOException, URISyntaxException {
         URI idbsnverFile = getDataFileURI(context, IDBSNVER_FILENAME);
         // Register the location as a service property for diagnostic purposes
@@ -94,7 +95,21 @@
             regProps.put(APIREGIONS_JOINGLOBAL, toglobal);
         }
 
-        enabledRegions = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(regionsProp.split(","))));
+        String defRegProp = context.getProperty(DEFAULT_REGIONS);
+        if (defRegProp != null) {
+            Set<String> defRegs = new HashSet<>();
+            for (String region : Arrays.asList(defRegProp.split(","))) {
+                if (region.length() > 0) {
+                    defRegs.add(region);
+                }
+            }
+            defaultRegions = Collections.unmodifiableSet(defRegs);
+            if (defaultRegions.size() > 0) {
+                regProps.put(DEFAULT_REGIONS, defaultRegions.toString());
+            }
+        } else {
+            defaultRegions = Collections.emptySet();
+        }
 
         // Make all maps and their contents unmodifiable
         bsnVerMap = unmodifiableMapToList(bvm);
@@ -230,8 +245,6 @@
 
     @Override
     public ResolverHook begin(Collection<BundleRevision> triggers) {
-        if (enabledRegions.isEmpty())
-            return null;
-        return new ResolverHookImpl(bsnVerMap, bundleFeatureMap, featureRegionMap, regionPackageMap);
+        return new ResolverHookImpl(bsnVerMap, bundleFeatureMap, featureRegionMap, regionPackageMap, defaultRegions);
     }
 }
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 0ec8c16..208a0cf 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
@@ -43,13 +43,15 @@
     final Map<String, Set<String>> bundleFeatureMap;
     final Map<String, Set<String>> featureRegionMap;
     final Map<String, Set<String>> regionPackageMap;
+    final Set<String> defaultRegions;
 
     ResolverHookImpl(Map<Entry<String, Version>, List<String>> bsnVerMap, Map<String, Set<String>> bundleFeatureMap,
-            Map<String, Set<String>> featureRegionMap, Map<String, Set<String>> regionPackageMap) {
+            Map<String, Set<String>> featureRegionMap, Map<String, Set<String>> regionPackageMap, Set<String> defaultRegions) {
         this.bsnVerMap = bsnVerMap;
         this.bundleFeatureMap = bundleFeatureMap;
         this.featureRegionMap = featureRegionMap;
         this.regionPackageMap = regionPackageMap;
+        this.defaultRegions = defaultRegions;
     }
 
     @Override
@@ -73,7 +75,7 @@
         String reqBundleName = reqBundle.getSymbolicName();
         Version reqBundleVersion = reqBundle.getVersion();
 
-        Set<String> reqRegions = new HashSet<>();
+        Set<String> reqRegions = new HashSet<>(defaultRegions);
         List<String> reqFeatures = new ArrayList<>();
         List<String> aids = bsnVerMap.get(new AbstractMap.SimpleEntry<String, Version>(reqBundleName, reqBundleVersion));
         if (aids != null) {
diff --git a/src/test/java/org/apache/sling/feature/apiregions/impl/RegionEnforcerTest.java b/src/test/java/org/apache/sling/feature/apiregions/impl/RegionEnforcerTest.java
index 938f3a6..ac1d77d 100644
--- a/src/test/java/org/apache/sling/feature/apiregions/impl/RegionEnforcerTest.java
+++ b/src/test/java/org/apache/sling/feature/apiregions/impl/RegionEnforcerTest.java
@@ -22,8 +22,12 @@
 import org.mockito.Mockito;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Version;
+import org.osgi.framework.hooks.resolver.ResolverHook;
 
 import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.net.URISyntaxException;
 import java.util.AbstractMap;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -36,6 +40,7 @@
 
 import static org.apache.sling.feature.apiregions.impl.RegionEnforcer.APIREGIONS_JOINGLOBAL;
 import static org.apache.sling.feature.apiregions.impl.RegionEnforcer.BUNDLE_FEATURE_FILENAME;
+import static org.apache.sling.feature.apiregions.impl.RegionEnforcer.DEFAULT_REGIONS;
 import static org.apache.sling.feature.apiregions.impl.RegionEnforcer.FEATURE_REGION_FILENAME;
 import static org.apache.sling.feature.apiregions.impl.RegionEnforcer.IDBSNVER_FILENAME;
 import static org.apache.sling.feature.apiregions.impl.RegionEnforcer.PROPERTIES_FILE_LOCATION;
@@ -51,7 +56,7 @@
         BundleContext ctx = Mockito.mock(BundleContext.class);
 
         try {
-            new RegionEnforcer(ctx, new Hashtable<String, Object>(), "*");
+            new RegionEnforcer(ctx, new Hashtable<String, Object>());
             fail("Expected exception. Enforcer is enabled but is missing configuration");
         } catch (Exception e) {
             // good
@@ -69,7 +74,7 @@
         Mockito.when(ctx.getProperty(PROPERTIES_RESOURCE_PREFIX + REGION_PACKAGE_FILENAME)).thenReturn(e);
 
         Hashtable<String, Object> props = new Hashtable<>();
-        RegionEnforcer re = new RegionEnforcer(ctx, props, "*");
+        RegionEnforcer re = new RegionEnforcer(ctx, props);
         assertEquals(2, re.bsnVerMap.size());
         assertEquals(Collections.singletonList("g:b1:1"),
                 re.bsnVerMap.get(new AbstractMap.SimpleEntry<String,Version>("b1", new Version(1,0,0))));
@@ -89,7 +94,7 @@
         Mockito.when(ctx.getProperty(PROPERTIES_RESOURCE_PREFIX + REGION_PACKAGE_FILENAME)).thenReturn(e);
 
         Hashtable<String, Object> props = new Hashtable<>();
-        RegionEnforcer re = new RegionEnforcer(ctx, props, "*");
+        RegionEnforcer re = new RegionEnforcer(ctx, props);
         assertEquals(3, re.bundleFeatureMap.size());
         assertEquals(Collections.singleton("org.sling:something:1.2.3:slingosgifeature:myclassifier"),
                 re.bundleFeatureMap.get("org.sling:b1:1"));
@@ -111,7 +116,7 @@
         Mockito.when(ctx.getProperty(PROPERTIES_RESOURCE_PREFIX + REGION_PACKAGE_FILENAME)).thenReturn(e);
 
         Hashtable<String, Object> props = new Hashtable<>();
-        RegionEnforcer re = new RegionEnforcer(ctx, props, "*");
+        RegionEnforcer re = new RegionEnforcer(ctx, props);
         assertEquals(2, re.featureRegionMap.size());
         assertEquals(Collections.singleton("global"),
                 re.featureRegionMap.get("an.other:feature:123"));
@@ -131,7 +136,7 @@
         Mockito.when(ctx.getProperty(PROPERTIES_RESOURCE_PREFIX + REGION_PACKAGE_FILENAME)).thenReturn(f);
 
         Hashtable<String, Object> props = new Hashtable<>();
-        RegionEnforcer re = new RegionEnforcer(ctx, props, "*");
+        RegionEnforcer re = new RegionEnforcer(ctx, props);
         assertEquals(2, re.regionPackageMap.size());
         assertEquals(Collections.singleton("xyz"),
                 re.regionPackageMap.get("internal"));
@@ -151,7 +156,7 @@
         Mockito.when(ctx.getProperty(PROPERTIES_RESOURCE_PREFIX + FEATURE_REGION_FILENAME)).thenReturn(e);
         Mockito.when(ctx.getProperty(PROPERTIES_RESOURCE_PREFIX + REGION_PACKAGE_FILENAME)).thenReturn(f);
 
-        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>(), "*");
+        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>());
         assertEquals(1, re.regionPackageMap.size());
         assertEquals(new HashSet<>(Arrays.asList("xyz", "a.b.c", "d.e.f", "test")),
                 re.regionPackageMap.get("global"));
@@ -169,7 +174,7 @@
         Mockito.when(ctx.getProperty(PROPERTIES_RESOURCE_PREFIX + REGION_PACKAGE_FILENAME)).
             thenReturn(getClass().getResource("/regions1.properties").getFile());
 
-        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>(), "*");
+        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>());
         assertTrue(re.bsnVerMap.size() > 0);
         assertTrue(re.bundleFeatureMap.size() > 0);
         assertTrue(re.featureRegionMap.size() > 0);
@@ -189,7 +194,7 @@
                 getFile()).getParentFile().toURI().toString();
         Mockito.when(ctx.getProperty(PROPERTIES_FILE_LOCATION)).thenReturn(location);
 
-        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>(), "*");
+        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>());
         assertTrue(re.bsnVerMap.size() > 0);
         assertTrue(re.bundleFeatureMap.size() > 0);
         assertTrue(re.featureRegionMap.size() > 0);
@@ -202,7 +207,7 @@
         Mockito.when(ctx.getProperty(PROPERTIES_FILE_LOCATION)).
             thenReturn("classloader://props1");
 
-        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>(), "*");
+        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>());
         assertTrue(re.bsnVerMap.size() > 0);
         assertTrue(re.bundleFeatureMap.size() > 0);
         assertTrue(re.featureRegionMap.size() > 0);
@@ -215,7 +220,7 @@
         Mockito.when(ctx.getProperty(PROPERTIES_FILE_LOCATION)).
             thenReturn("classloader://props2");
 
-        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>(), "*");
+        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>());
         assertEquals(Arrays.asList("r0", "r1", "r2", "r3"),
                 new ArrayList<>(re.featureRegionMap.get("org.sling:something:1.2.3")));
     }
@@ -226,7 +231,7 @@
         Mockito.when(ctx.getProperty(PROPERTIES_FILE_LOCATION)).
             thenReturn("classloader://props1");
 
-        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>(), "*");
+        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>());
         assertTrue(re.bsnVerMap.size() > 0);
         assertBSNVerMapUnmodifiable(re.bsnVerMap);
         assertTrue(re.bundleFeatureMap.size() > 0);
@@ -237,6 +242,30 @@
         assertMapUnmodifiable(re.regionPackageMap);
     }
 
+    @Test
+    public void testDefaultRegions() throws Exception {
+        testDefaultRegions("foo.bar,foo.zar", new HashSet<>(Arrays.asList("foo.bar", "foo.zar")));
+        testDefaultRegions("test", Collections.singleton("test"));
+        testDefaultRegions("", Collections.emptySet());
+        testDefaultRegions(null, Collections.emptySet());
+    }
+
+    @SuppressWarnings("unchecked")
+    private void testDefaultRegions(String defProp, Set<String> expected)
+            throws IOException, URISyntaxException, NoSuchFieldException, IllegalAccessException {
+        BundleContext ctx = Mockito.mock(BundleContext.class);
+        Mockito.when(ctx.getProperty(DEFAULT_REGIONS)).thenReturn(defProp);
+        Mockito.when(ctx.getProperty(PROPERTIES_FILE_LOCATION)).
+        thenReturn("classloader://props1");
+
+        RegionEnforcer re = new RegionEnforcer(ctx, new Hashtable<String, Object>());
+        ResolverHook hook = re.begin(Collections.emptySet());
+        Field f = ResolverHookImpl.class.getDeclaredField("defaultRegions");
+        f.setAccessible(true);
+
+        assertEquals(expected, f.get(hook));
+    }
+
     private void assertBSNVerMapUnmodifiable(Map<Map.Entry<String, Version>, List<String>> m) {
         Map.Entry<Map.Entry<String, Version>, List<String>> entry = m.entrySet().iterator().next();
         try {
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 13c1248..2910492 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
@@ -60,7 +60,7 @@
 
         Map<String, Set<String>> rpmap = new HashMap<>();
 
-        ResolverHookImpl rh = new ResolverHookImpl(bsnvermap, bfmap, frmap, rpmap);
+        ResolverHookImpl rh = new ResolverHookImpl(bsnvermap, bfmap, frmap, rpmap, Collections.singleton("*"));
 
         // b2 is in r2, it requires a capability that is provided by b1.
         // b1 is not in any region so it can provide access.
@@ -92,7 +92,7 @@
         rpmap.put("r1", Collections.singleton("org.apache.sling.test"));
         rpmap.put("r3", Collections.singleton("org.apache.sling.toast"));
 
-        ResolverHookImpl rh = new ResolverHookImpl(bsnvermap, bfmap, frmap, rpmap);
+        ResolverHookImpl rh = new ResolverHookImpl(bsnvermap, bfmap, frmap, rpmap, Collections.singleton(""));
 
         // b2 is in r2, it requires a capability that is provided by b1 in r1. However since b1 is also
         // in r2, r2 inherits all the capabilities provided by regions before r2, which includes r2. So
@@ -158,7 +158,7 @@
         rpmap.put(RegionEnforcer.GLOBAL_REGION, Collections.singleton("org.bar.tar"));
         rpmap.put("r3", Collections.singleton("xyz"));
 
-        ResolverHookImpl rh = new ResolverHookImpl(bsnvermap, bfmap, frmap, rpmap);
+        ResolverHookImpl rh = new ResolverHookImpl(bsnvermap, bfmap, frmap, rpmap, Collections.emptySet());
 
         // Check that we can get the capability from another bundle in the same region
         // where that region exports the package
@@ -286,7 +286,7 @@
 
         ResolverHookImpl rh = new ResolverHookImpl(
                 Collections.<Map.Entry<String, Version>, List<String>>emptyMap(),
-                Collections.<String, Set<String>>emptyMap(), featureRegionMap, regionPackageMap);
+                Collections.<String, Set<String>>emptyMap(), featureRegionMap, regionPackageMap, Collections.emptySet());
 
         assertEquals(Collections.emptyList(), rh.getRegionsForPackage(null, "f1"));
         assertEquals(Collections.emptyList(), rh.getRegionsForPackage("org.foo", "f1"));
@@ -299,6 +299,52 @@
             rh.getRegionsForPackage("a.b.c", "f2"));
     }
 
+    @Test
+    public void testDefaultRegions() {
+        Map<Entry<String, Version>, List<String>> bsnvermap = new HashMap<>();
+        bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>("b98", new Version(1, 0, 0)),
+                Collections.singletonList("b98"));
+        bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>("b99", new Version(1, 2, 3)),
+                Collections.singletonList("b99"));
+        bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>("b100", new Version(4, 5, 6)),
+                Collections.singletonList("b100"));
+
+        Map<String, Set<String>> bfmap = new HashMap<>();
+        bfmap.put("b98", Collections.singleton("f2"));
+        bfmap.put("b100", Collections.singleton("f1"));
+
+        Map<String, Set<String>> frmap = new HashMap<>();
+        frmap.put("f1", Collections.singleton("r1"));
+        frmap.put("f2", Collections.singleton("r2"));
+
+        Map<String, Set<String>> rpmap = new HashMap<>();
+        rpmap.put("r1", Collections.singleton("org.test"));
+        rpmap.put("r2", Collections.singleton("org.test"));
+
+        ResolverHookImpl rh = new ResolverHookImpl(bsnvermap, bfmap, frmap, rpmap,
+                new HashSet<>(Arrays.asList("r1", "r3")));
+
+        // b99 is not in any region itself and tries to resolve to b100 which is in r1
+        // b99 can resolve to b100 because 'r1' is listed as a default region in the
+        // ResolverHook.
+        BundleRequirement req = mockRequirement("b99", bsnvermap);
+        BundleCapability cap = mockCapability("org.test", "b100", bsnvermap);
+        List<BundleCapability> candidates = new ArrayList<>(Arrays.asList(cap));
+        rh.filterMatches(req, candidates);
+        assertEquals("b99 should be able to wire to b100, as the default region is r1, which is what b100 is in. ",
+                Collections.singletonList(cap), candidates);
+
+        // b99 is not in any region and tries to resolve to b98, which is in r2
+        // b99 can't resolve because b2 is not in the default regions and it's not in
+        // any other regions itself.
+        BundleRequirement req1 = mockRequirement("b99", bsnvermap);
+        BundleCapability cap1 = mockCapability("org.test", "b98", bsnvermap);
+        List<BundleCapability> candidates1 = new ArrayList<>(Arrays.asList(cap1));
+        rh.filterMatches(req1, candidates1);
+        assertEquals("b99 should not be able to wire to b98, as this is in region r2, which is not in the default regions r1 and r3",
+                0, candidates1.size());
+    }
+
     private BundleCapability mockCapability(String pkgName, String bid, Map<Entry<String, Version>, List<String>> bsnvermap) {
         for (Map.Entry<Map.Entry<String, Version>, List<String>> entry : bsnvermap.entrySet()) {
             if (entry.getValue().contains(bid)) {
diff --git a/src/test/resources/props1/bundleOrigins.properties b/src/test/resources/props1/bundles.properties
similarity index 100%
rename from src/test/resources/props1/bundleOrigins.properties
rename to src/test/resources/props1/bundles.properties
diff --git a/src/test/resources/props1/regionOrigins.properties b/src/test/resources/props1/features.properties
similarity index 100%
rename from src/test/resources/props1/regionOrigins.properties
rename to src/test/resources/props1/features.properties
diff --git a/src/test/resources/props2/bundleOrigins.properties b/src/test/resources/props2/bundles.properties
similarity index 100%
rename from src/test/resources/props2/bundleOrigins.properties
rename to src/test/resources/props2/bundles.properties
diff --git a/src/test/resources/props2/regionOrigins.properties b/src/test/resources/props2/features.properties
similarity index 100%
rename from src/test/resources/props2/regionOrigins.properties
rename to src/test/resources/props2/features.properties
diff --git a/src/test/resources/props3/bundleOrigins.properties b/src/test/resources/props3/bundles.properties
similarity index 100%
rename from src/test/resources/props3/bundleOrigins.properties
rename to src/test/resources/props3/bundles.properties
diff --git a/src/test/resources/props3/regionOrigins.properties b/src/test/resources/props3/features.properties
similarity index 100%
rename from src/test/resources/props3/regionOrigins.properties
rename to src/test/resources/props3/features.properties