Merge pull request #13 from bosschaert/SLING-9521-sq

SLING-9521 Packages exported in earlier API Regions are not available to later API Regions
diff --git a/src/main/java/org/apache/sling/feature/apiregions/impl/RegionConfiguration.java b/src/main/java/org/apache/sling/feature/apiregions/impl/RegionConfiguration.java
index f0f25e6..acb7720 100644
--- a/src/main/java/org/apache/sling/feature/apiregions/impl/RegionConfiguration.java
+++ b/src/main/java/org/apache/sling/feature/apiregions/impl/RegionConfiguration.java
@@ -37,7 +37,6 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Hashtable;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -45,6 +44,7 @@
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
+import java.util.function.Supplier;
 import java.util.logging.Level;
 
 import org.osgi.framework.BundleContext;
@@ -52,10 +52,11 @@
 
 class RegionConfiguration {
     private static final String BUNDLE_LOCATION_TO_FEATURE_FILE = "bundleLocationToFeature.properties";
+    private static final String REGION_ORDER = "__region.order__";
 
     volatile Map<Map.Entry<String, Version>, List<String>> bsnVerMap;
     volatile Map<String, Set<String>> bundleFeatureMap;
-    volatile Map<String, Set<String>> featureRegionMap;
+    volatile Map<String, List<String>> featureRegionMap;
     volatile Map<String, Set<String>> regionPackageMap;
 
     final Set<String> defaultRegions;
@@ -65,8 +66,9 @@
 
     private final Map<Map.Entry<String, Version>, List<String>> baseBsnVerMap;
     private final Map<String, Set<String>> baseBundleFeatureMap;
-    private final Map<String, Set<String>> baseFeatureRegionMap;
+    private final Map<String, List<String>> baseFeatureRegionMap;
     private final Map<String, Set<String>> baseRegionPackageMap;
+    private final List<String> globalRegionOrder;
 
     // This field stores the association between bundle location and the configuration
     // to be used. The configuration is based on bsn+version. If the bundle is updated
@@ -78,13 +80,15 @@
     private final String toGlobalConfig;
 
     RegionConfiguration(Map<Entry<String, Version>, List<String>> bsnVerMap, Map<String, Set<String>> bundleFeatureMap,
-                        Map<String, Set<String>> featureRegionMap, Map<String, Set<String>> regionPackageMap, Set<String> defaultRegions) {
+                        Map<String, List<String>> featureRegionMap, Map<String, Set<String>> regionPackageMap, Set<String> defaultRegions) {
         this.defaultRegions = defaultRegions;
 
         this.baseBsnVerMap = new HashMap<>(bsnVerMap);
         this.baseBundleFeatureMap = new HashMap<>(bundleFeatureMap);
         this.baseFeatureRegionMap = new HashMap<>(featureRegionMap);
         this.baseRegionPackageMap = new HashMap<>(regionPackageMap);
+        this.globalRegionOrder = new ArrayList<>(this.baseFeatureRegionMap.getOrDefault(REGION_ORDER, Collections.emptyList()));
+        this.baseFeatureRegionMap.remove(REGION_ORDER);
 
         this.toGlobalConfig = null;
 
@@ -106,7 +110,7 @@
         URI featuresFile = getDataFileURI(context, RegionConstants.FEATURE_REGION_FILENAME);
         // Register the location as a service property for diagnostic purposes
         regProps.put(RegionConstants.FEATURE_REGION_FILENAME, featuresFile.toString());
-        Map<String, Set<String>> frm = populateFeatureRegionMap(featuresFile);
+        Map<String, List<String>> frm = populateFeatureRegionMap(featuresFile);
 
         URI regionsFile = getDataFileURI(context, RegionConstants.REGION_PACKAGE_FILENAME);
         // Register the location as a service property for diagnostic purposes
@@ -118,6 +122,8 @@
         this.baseBundleFeatureMap = bfm;
         this.baseFeatureRegionMap = frm;
         this.baseRegionPackageMap = rpm;
+        this.globalRegionOrder = new ArrayList<>(this.baseFeatureRegionMap.getOrDefault(REGION_ORDER, Collections.emptyList()));
+        this.baseFeatureRegionMap.remove(REGION_ORDER);
 
         this.toGlobalConfig = context.getProperty(RegionConstants.APIREGIONS_JOINGLOBAL);
         if ( this.toGlobalConfig != null ) {
@@ -206,7 +212,7 @@
     private synchronized void updateConfiguration() {
         final Map<Entry<String, Version>, List<String>> bvm = cloneMapOfLists(this.baseBsnVerMap);
         final Map<String, Set<String>> bfm = cloneMapOfSets(this.baseBundleFeatureMap);
-        final Map<String, Set<String>> frm = cloneMapOfSets(this.baseFeatureRegionMap);
+        final Map<String, List<String>> frm = cloneMapOfLists(this.baseFeatureRegionMap);
         final Map<String, Set<String>> rpm = cloneMapOfSets(this.baseRegionPackageMap);
 
         // apply configurations
@@ -227,19 +233,19 @@
             // bundle id to features
             valObj = props.get(RegionConstants.PROP_bundleFeatures);
             if ( valObj != null ) {
-                handleMapConfig(valObj, bfm);
+                handleMapConfig(valObj, bfm, HashSet::new);
             }
 
             // feature id to regions
             valObj = props.get(RegionConstants.PROP_featureRegions);
             if ( valObj != null ) {
-                handleMapConfig(valObj, frm);
+                handleMapConfig(valObj, frm, ArrayList::new);
             }
 
             // region to packages
             valObj = props.get(RegionConstants.PROP_regionPackage);
             if ( valObj != null ) {
-                handleMapConfig(valObj, rpm);
+                handleMapConfig(valObj, rpm, HashSet::new);
             }
         }
 
@@ -251,16 +257,16 @@
         // Make all maps and their contents unmodifiable
         bsnVerMap = unmodifiableMapToList(bvm);
         bundleFeatureMap = unmodifiableMapToSet(bfm);
-        featureRegionMap = unmodifiableMapToSet(frm);
+        featureRegionMap = unmodifiableMapToList(frm);
         regionPackageMap = unmodifiableMapToSet(rpm);
     }
 
-    private void handleMapConfig(Object valObj, Map<String, Set<String>> map) {
+    private <T extends Collection<String>> void handleMapConfig(Object valObj, Map<String, T> map, Supplier<T> constructor) {
         for(final String val : convert(valObj)) {
             final String[] parts = val.split("=");
             final String n = parts[0];
             final String[] features = parts[1].split(",");
-            addValuesToMap(map, n, Arrays.asList(features));
+            addValuesToMap(map, n, Arrays.asList(features), constructor);
         }
     }
 
@@ -275,7 +281,7 @@
     private static <K,V> Map<K, Set<V>> cloneMapOfSets(Map<K, Set<V>> m) {
         final Map<K, Set<V>> newMap = new HashMap<>();
         for (Map.Entry<K, Set<V>> entry : m.entrySet()) {
-            newMap.put(entry.getKey(), new LinkedHashSet<>(entry.getValue()));
+            newMap.put(entry.getKey(), new HashSet<>(entry.getValue()));
         }
         return newMap;
     }
@@ -300,7 +306,7 @@
             if (packages == null)
                 continue;
 
-            addValuesToMap(rpm, RegionConstants.GLOBAL_REGION, packages);
+            addValuesToMap(rpm, RegionConstants.GLOBAL_REGION, packages, HashSet::new);
             rpm.remove(region);
         }
     }
@@ -337,19 +343,19 @@
     }
 
     private static Map<String, Set<String>> populateBundleFeatureMap(URI bundlesFile) throws IOException {
-        return loadMap(bundlesFile);
+        return loadMap(bundlesFile, HashSet::new);
     }
 
-    private static Map<String, Set<String>> populateFeatureRegionMap(URI featuresFile) throws IOException {
-        return loadMap(featuresFile);
+    private static Map<String, List<String>> populateFeatureRegionMap(URI featuresFile) throws IOException {
+        return loadMap(featuresFile, ArrayList::new);
     }
 
     private static Map<String, Set<String>> populateRegionPackageMap(URI regionsFile) throws IOException {
-        return loadMap(regionsFile);
+        return loadMap(regionsFile, HashSet::new);
     }
 
-    private static Map<String, Set<String>> loadMap(URI propsFile) throws IOException {
-        Map<String, Set<String>> m = new HashMap<>();
+    private static <T extends Collection<String>> Map<String, T> loadMap(URI propsFile, Supplier<T> constructor) throws IOException {
+        Map<String, T> m = new HashMap<>();
 
         Properties p = new Properties();
         try (InputStream is = propsFile.toURL().openStream()) {
@@ -358,16 +364,16 @@
 
         for (String n : p.stringPropertyNames()) {
             String[] values = p.getProperty(n).split(",");
-            addValuesToMap(m, n, Arrays.asList(values));
+            addValuesToMap(m, n, Arrays.asList(values), constructor);
         }
 
         return m;
     }
 
-    private static void addValuesToMap(Map<String, Set<String>> map, String key, Collection<String> values) {
-        Set<String> bf = map.get(key);
+    private static <T extends Collection<String>> void addValuesToMap(Map<String, T> map, String key, Collection<String> values, Supplier<T> constructor) {
+        T bf = map.get(key);
         if (bf == null) {
-            bf = new LinkedHashSet<>(); // It's important that the insertion order is maintained.
+            bf = constructor.get();
             map.put(key, bf);
         }
         bf.addAll(values);
@@ -428,7 +434,7 @@
         return bundleFeatureMap;
     }
 
-    public Map<String, Set<String>> getFeatureRegionMap() {
+    public Map<String, List<String>> getFeatureRegionMap() {
         return featureRegionMap;
     }
 
@@ -440,6 +446,10 @@
         return defaultRegions;
     }
 
+    public List<String> getGlobalRegionOrder() {
+        return globalRegionOrder;
+    }
+
     public Dictionary<String, Object> getRegistrationProperties() {
         return regProps;
     }
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 fe5d634..a614851 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
@@ -78,7 +78,7 @@
         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);
+            List<String> fr = this.configuration.getFeatureRegionMap().get(feature);
             if (fr != null) {
                 if (bareReqRegions == null)
                     bareReqRegions = new HashSet<>();
@@ -131,7 +131,7 @@
                     continue nextCapability;
                 }
 
-                Set<String> capRegions = this.configuration.getFeatureRegionMap().get(capFeat);
+                List<String> capRegions = this.configuration.getFeatureRegionMap().get(capFeat);
                 if (capRegions == null || capRegions.size() == 0) {
                     // If the feature hosting the capability has no regions defined, everyone can access
                     coveredCaps.put(bc, RegionConstants.GLOBAL_REGION);
@@ -139,7 +139,7 @@
                 }
                 bcFeatureMap.put(bc, capFeat);
 
-                List<String> sharedRegions = new ArrayList<>(reqRegions);
+                List<String> sharedRegions = new ArrayList<>(getRegionsAndAncestors(reqRegions));
                 sharedRegions.retainAll(capRegions);
 
                 // Look at specific regions first as they take precedence over the global region
@@ -200,6 +200,28 @@
         }
     }
 
+    // Get the a set of the regions plus their ancestors. They are obtained from the global region order.
+    private Set<String> getRegionsAndAncestors(Set<String> regions) {
+        Set<String> s = new HashSet<>();
+
+        for (String region : regions) {
+            s.add(region);
+
+            if (configuration.getGlobalRegionOrder().contains(region)) {
+                for (String r : configuration.getGlobalRegionOrder()) {
+                    if (r.equals(region)) {
+                        break;
+                    }
+                    s.add(r);
+                }
+            } else {
+                Activator.LOG.log(Level.WARNING, "Global API Region order " + configuration.getGlobalRegionOrder() +
+                        " does not contain region: " + region);
+            }
+        }
+        return s;
+    }
+
     /**
      * Check if the package is exported in the global region
      * @param packageName The package
@@ -207,7 +229,7 @@
      * @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);
+        List<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)) {
@@ -311,7 +333,7 @@
         if (packageName == null)
             return Collections.emptyList();
 
-        Set<String> regions = this.configuration.getFeatureRegionMap().get(feature);
+        List<String> regions = this.configuration.getFeatureRegionMap().get(feature);
         if (regions == null)
             return Collections.emptyList();
 
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 714eba9..db3ca70 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
@@ -27,6 +27,7 @@
 import static org.apache.sling.feature.apiregions.impl.RegionConstants.PROPERTIES_RESOURCE_PREFIX;
 import static org.apache.sling.feature.apiregions.impl.RegionConstants.REGION_PACKAGE_FILENAME;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -37,6 +38,7 @@
 import java.util.AbstractMap;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Dictionary;
 import java.util.HashSet;
@@ -193,9 +195,9 @@
 
         RegionConfiguration re = new RegionConfiguration(ctx);
         assertEquals(2, re.featureRegionMap.size());
-        assertEquals(Collections.singleton("global"),
+        assertEquals(Collections.singletonList("global"),
                 re.featureRegionMap.get("an.other:feature:123"));
-        assertEquals(new HashSet<>(Arrays.asList("global", "internal")),
+        assertEquals(Arrays.asList("global", "internal"),
                 re.featureRegionMap.get("org.sling:something:1.2.3"));
         assertEquals(f,  re.getRegistrationProperties().get(FEATURE_REGION_FILENAME));
     }
@@ -217,18 +219,18 @@
         re.setConfig("new.config", props);
 
         assertEquals(3, re.featureRegionMap.size());
-        assertEquals(Collections.singleton("newregion"),
+        assertEquals(Collections.singletonList("newregion"),
                 re.featureRegionMap.get("fg1:fa1:3.0"));
-        assertEquals(Collections.singleton("global"),
+        assertEquals(Collections.singletonList("global"),
                 re.featureRegionMap.get("an.other:feature:123"));
-        assertEquals(new HashSet<>(Arrays.asList("global", "internal")),
+        assertEquals(Arrays.asList("global", "internal"),
                 re.featureRegionMap.get("org.sling:something:1.2.3"));
 
         re.removeConfig("new.config");
         assertEquals(2, re.featureRegionMap.size());
-        assertEquals(Collections.singleton("global"),
+        assertEquals(Collections.singletonList("global"),
                 re.featureRegionMap.get("an.other:feature:123"));
-        assertEquals(new HashSet<>(Arrays.asList("global", "internal")),
+        assertEquals(Arrays.asList("global", "internal"),
                 re.featureRegionMap.get("org.sling:something:1.2.3"));
     }
 
@@ -493,6 +495,19 @@
                 expected, caps2);
     }
 
+    @Test
+    public void testRegionOrderProperty() throws Exception {
+        BundleContext ctx = Mockito.mock(BundleContext.class);
+        Mockito.when(ctx.getBundle()).thenReturn(Mockito.mock(Bundle.class));
+        Mockito.when(ctx.getProperty(PROPERTIES_FILE_LOCATION)).
+            thenReturn("classloader://props1");
+
+        RegionConfiguration re = new RegionConfiguration(ctx);
+        assertEquals(Arrays.asList("global", "internal"), re.getGlobalRegionOrder());
+        assertEquals(2, re.getFeatureRegionMap().size());
+        assertNull(re.getFeatureRegionMap().get("__region.order__"));
+    }
+
     private BundleRequirement mockRequirement(String bsn, Version bver, BundleContext mockContext) {
         BundleRevision br = mockBundleRevision(bsn, bver, mockContext);
 
@@ -567,10 +582,11 @@
         }
     }
 
-    private void assertMapUnmodifiable(Map<String, Set<String>> m) {
-        Map.Entry<String, Set<String>> entry = m.entrySet().iterator().next();
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    private void assertMapUnmodifiable(Map<String, ? extends Collection<String>> m) {
+        Map.Entry<String, ? extends Collection<String>> entry = m.entrySet().iterator().next();
         try {
-            Set<String> s = entry.getValue();
+            Collection<String> s = entry.getValue();
             s.add("testing");
             fail("Changing a value should have thrown an exception");
         } catch (Exception ex) {
@@ -578,7 +594,8 @@
         }
 
         try {
-            m.put("foo", Collections.<String>emptySet());
+            Map m2 = m;
+            m2.put("foo", Collections.<String>emptySet());
             fail("Adding a new value should have thrown an exception");
         } catch (Exception ex) {
             // good
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 bf9d5fb..3f27cb9 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
@@ -54,8 +54,9 @@
         bfmap.put("b1", Collections.singleton("f1"));
         bfmap.put("b2", Collections.singleton("f2"));
 
-        Map<String, Set<String>> frmap = new HashMap<>();
-        frmap.put("f2", Collections.singleton("r2"));
+        Map<String, List<String>> frmap = new HashMap<>();
+        frmap.put("f2", Collections.singletonList("r2"));
+        frmap.put("__region.order__", Arrays.asList("global", "r2"));
 
         Map<String, Set<String>> rpmap = new HashMap<>();
 
@@ -81,8 +82,9 @@
         Map<String, Set<String>> bfmap = new HashMap<>();
         bfmap.put("b2", Collections.singleton("f2"));
 
-        Map<String, Set<String>> frmap = new HashMap<>();
-        frmap.put("f2", Collections.singleton("r2"));
+        Map<String, List<String>> frmap = new HashMap<>();
+        frmap.put("f2", Collections.singletonList("r2"));
+        frmap.put("__region.order__", Arrays.asList("global", "r2"));
 
         Map<String, Set<String>> rpmap = new HashMap<>();
 
@@ -109,9 +111,10 @@
         bfmap.put("b1", Collections.singleton("f1"));
         bfmap.put("b2", Collections.singleton("f2"));
 
-        Map<String, Set<String>> frmap = new HashMap<>();
-        frmap.put("f1", Collections.emptySet());
-        frmap.put("f2", Collections.singleton("r2"));
+        Map<String, List<String>> frmap = new HashMap<>();
+        frmap.put("f1", Collections.emptyList());
+        frmap.put("f2", Collections.singletonList("r2"));
+        frmap.put("__region.order__", Arrays.asList("global", "r2"));
 
         Map<String, Set<String>> rpmap = new HashMap<>();
 
@@ -138,9 +141,10 @@
         bfmap.put("b1", Collections.singleton("f1"));
         bfmap.put("b2", Collections.singleton("f2"));
 
-        Map<String, Set<String>> frmap = new HashMap<>();
-        frmap.put("f1", Collections.singleton("r2"));
-        frmap.put("f2", Collections.singleton("r2"));
+        Map<String, List<String>> frmap = new HashMap<>();
+        frmap.put("f1", Collections.singletonList("r2"));
+        frmap.put("f2", Collections.singletonList("r2"));
+        frmap.put("__region.order__", Arrays.asList("global", "r2"));
 
         Map<String, Set<String>> rpmap = new HashMap<>();
 
@@ -173,10 +177,59 @@
         bfmap.put("b11", Collections.singleton("f11"));
         bfmap.put("b2", Collections.singleton("f2"));
 
-        Map<String, Set<String>> frmap = new HashMap<>();
-        frmap.put("f10", new HashSet<>(Arrays.asList("r2", "r3")));
-        frmap.put("f11", new HashSet<>(Arrays.asList("r1", "r2")));
-        frmap.put("f2", Collections.singleton("r1"));
+        Map<String, List<String>> frmap = new HashMap<>();
+        frmap.put("f10", Arrays.asList("r2", "r3"));
+        frmap.put("f11", Arrays.asList("r1", "r2"));
+        frmap.put("f2", Collections.singletonList("r1"));
+        frmap.put("__region.order__", Arrays.asList("global", "r1", "r2", "r3", "r4"));
+
+        Map<String, Set<String>> rpmap = new HashMap<>();
+        rpmap.put("r1", Collections.singleton("org.foo.bar"));
+        rpmap.put("r2", new HashSet<>(Arrays.asList("xxx", "yyy", "zzz")));
+        rpmap.put("r3", new HashSet<>(Arrays.asList("org.foo.bar", "zzz")));
+
+        ResolverHookImpl rh = new ResolverHookImpl(new RegionConfiguration(bsnvermap, bfmap, frmap, rpmap, Collections.singleton("global")));
+
+        // b2 needs to resolve to 'org.foo.bar' package. b2 is in region r1.
+        // The package is provided by 3 bundles:
+        //   b10 provides it but is not in a matching region
+        //   b11 provides it and has a matching region
+        //   b19 provides it in the global region
+        // Only b11 should provide the capability. Even though b19 provides it from the global region, if there is an overlapping
+        // specific region then the global region should not be used
+        BundleRequirement req1 = mockRequirement("b2", bsnvermap);
+        BundleCapability cap1 = mockCapability("org.foo.bar", "b10", bsnvermap);
+        BundleCapability cap2 = mockCapability("org.foo.bar", "b11", bsnvermap);
+        BundleCapability cap3 = mockCapability("org.foo.bar", "b19", bsnvermap);
+        List<BundleCapability> candidates1 = new ArrayList<>(Arrays.asList(cap1, cap2, cap3));
+        rh.filterMatches(req1, candidates1);
+
+        assertEquals("Only the capability coming from bundle b11 should be selected, b10 is in a different region and b19 is in global "
+                + "which should be excluded as there is a capability in a matching region.",
+                Collections.singletonList(cap2), candidates1);
+    }
+
+    @Test
+    public void testRegionHasPrecedenceOverGlobalRegionOrderNotSet() {
+        Map<Entry<String, Version>, List<String>> bsnvermap = new HashMap<>();
+        bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
+                "providing.bundle.otherfeature", new Version(1,0,0)), Collections.singletonList("b10"));
+        bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
+                "providing.bundle.infeature", new Version(1,0,0)), Collections.singletonList("b11"));
+        bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
+                "providing.bundle.inglobal", new Version(1,0,0)), Collections.singletonList("b19"));
+        bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
+                "requiring.bundle", new Version(1,0,0)), Collections.singletonList("b2"));
+
+        Map<String, Set<String>> bfmap = new HashMap<>();
+        bfmap.put("b10", Collections.singleton("f10"));
+        bfmap.put("b11", Collections.singleton("f11"));
+        bfmap.put("b2", Collections.singleton("f2"));
+
+        Map<String, List<String>> frmap = new HashMap<>();
+        frmap.put("f10", Arrays.asList("r2", "r3"));
+        frmap.put("f11", Arrays.asList("r1", "r2"));
+        frmap.put("f2", Collections.singletonList("r1"));
 
         Map<String, Set<String>> rpmap = new HashMap<>();
         rpmap.put("r1", Collections.singleton("org.foo.bar"));
@@ -215,8 +268,9 @@
         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, List<String>> frmap = new HashMap<>();
+        frmap.put("f1", Collections.emptyList());
+        frmap.put("__region.order__", Arrays.asList("global", "r1"));
 
         Map<String, Set<String>> rpmap = new HashMap<>();
 
@@ -247,8 +301,9 @@
         bfmap.put("b11", Collections.singleton("f1"));
         bfmap.put("b2", Collections.singleton("f1"));
 
-        Map<String, Set<String>> frmap = new HashMap<>();
-        frmap.put("f1", Collections.singleton("r1"));
+        Map<String, List<String>> frmap = new HashMap<>();
+        frmap.put("f1", Collections.singletonList("r1"));
+        frmap.put("__region.order__", Arrays.asList("global", "r1"));
 
         Map<String, Set<String>> rpmap = new HashMap<>();
         rpmap.put("r1", Collections.emptySet());
@@ -284,8 +339,9 @@
         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, List<String>> frmap = new HashMap<>();
+        frmap.put("f1", Arrays.asList("r1", "global"));
+        frmap.put("__region.order__", Arrays.asList("global", "r1"));
 
         Map<String, Set<String>> rpmap = new HashMap<>();
         rpmap.put("r1", Collections.singleton("org.blah.blah"));
@@ -323,9 +379,10 @@
         bfmap.put("b10", Collections.singleton("f10"));
         bfmap.put("b2", Collections.singleton("f2"));
 
-        Map<String, Set<String>> frmap = new HashMap<>();
-        frmap.put("f10", new HashSet<>(Arrays.asList("r2", "r3")));
-        frmap.put("f2", Collections.singleton("r1"));
+        Map<String, List<String>> frmap = new HashMap<>();
+        frmap.put("f10", Arrays.asList("r2", "r3"));
+        frmap.put("f2", Collections.singletonList("r1"));
+        frmap.put("__region.order__", Arrays.asList("global", "r1", "r2", "r3"));
 
         Map<String, Set<String>> rpmap = new HashMap<>();
         rpmap.put("r1", Collections.emptySet());
@@ -362,7 +419,7 @@
                 "requiring.bundle", new Version(1,0,0)), Collections.singletonList("b2"));
 
         Map<String, Set<String>> bfmap = new HashMap<>();
-        Map<String, Set<String>> frmap = new HashMap<>();
+        Map<String, List<String>> frmap = new HashMap<>();
         Map<String, Set<String>> rpmap = new HashMap<>();
 
         ResolverHookImpl rh = new ResolverHookImpl(new RegionConfiguration(bsnvermap, bfmap, frmap, rpmap, Collections.singleton("global")));
@@ -411,12 +468,13 @@
         bfmap.put("b19", Collections.singleton("f3"));
         bfmap.put("b20", Collections.singleton("f4"));
 
-        Map<String, Set<String>> frmap = new HashMap<>();
-        frmap.put("f", new HashSet<>(Arrays.asList("r1", "r2", RegionConstants.GLOBAL_REGION)));
-        frmap.put("f1", Collections.singleton("r1"));
-        frmap.put("f2", Collections.singleton("r2"));
-        frmap.put("f3", Collections.singleton("r3"));
-        frmap.put("f4", Collections.singleton("r3"));
+        Map<String, List<String>> frmap = new HashMap<>();
+        frmap.put("f", Arrays.asList("r1", "r2", RegionConstants.GLOBAL_REGION));
+        frmap.put("f1", Collections.singletonList("r1"));
+        frmap.put("f2", Collections.singletonList("r2"));
+        frmap.put("f3", Collections.singletonList("r3"));
+        frmap.put("f4", Collections.singletonList("r3"));
+        frmap.put("__region.order__", Arrays.asList("global", "r1", "r2", "r3"));
 
         Map<String, Set<String>> rpmap = new HashMap<>();
         rpmap.put("r0", Collections.singleton("org.bar"));
@@ -550,11 +608,10 @@
         bfmap.put("b1", Collections.singleton("f1"));
         bfmap.put("b2", Collections.singleton("f2"));
 
-        Map<String, Set<String>> frmap = new HashMap<>();
-        frmap.put("f1", new HashSet<>(Arrays.asList(
-                RegionConstants.GLOBAL_REGION, "org.foo.blah")));
-        frmap.put("f2", new HashSet<>(Arrays.asList("org.foo.bar",
-                RegionConstants.GLOBAL_REGION, "org.foo.blah")));
+        Map<String, List<String>> frmap = new HashMap<>();
+        frmap.put("f1", Arrays.asList(RegionConstants.GLOBAL_REGION, "org.foo.blah"));
+        frmap.put("f2", Arrays.asList("org.foo.bar", RegionConstants.GLOBAL_REGION, "org.foo.blah"));
+        frmap.put("__region.order__", Arrays.asList("global", "org.foo.blah", "org.foo.bar"));
 
         Map<String, Set<String>> rpmap = new HashMap<>();
         rpmap.put("org.foo.bar", Collections.singleton("org.test"));
@@ -572,8 +629,8 @@
 
     @Test
     public void testGetRegionsForPackage() {
-        Set<String> regions = new HashSet<>(Arrays.asList("r1", "r2", "r3"));
-        Map<String, Set<String>> featureRegionMap = Collections.singletonMap("f2", regions);
+        List<String> regions = Arrays.asList("r1", "r2", "r3");
+        Map<String, List<String>> featureRegionMap = Collections.singletonMap("f2", regions);
         Map<String, Set<String>> regionPackageMap = new HashMap<>();
 
         regionPackageMap.put("r2", Collections.singleton("a.b.c"));
@@ -609,16 +666,17 @@
         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, List<String>> frmap = new HashMap<>();
+        frmap.put("f1", Collections.singletonList("r1"));
+        frmap.put("f2", Collections.singletonList("r2"));
+        frmap.put("__region.order__", Arrays.asList("global", "r0", "r1", "r2", "r3"));
 
         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(new RegionConfiguration(bsnvermap, bfmap, frmap, rpmap,
-                new HashSet<>(Arrays.asList("r1", "r3"))));
+                new HashSet<>(Arrays.asList("r0", "r1"))));
 
         // 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
@@ -703,6 +761,57 @@
         assertEquals(expected, rhi4.getFeaturesForBundle(b4));
     }
 
+    @Test
+    public void testRegionOrderInheritance() {
+        Map<Entry<String, Version>, List<String>> bsnvermap = new HashMap<>();
+        bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
+                "b1", new Version(1,0,0)), Collections.singletonList("b1"));
+        bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
+                "b2", new Version(1,0,0)), Collections.singletonList("b2"));
+
+        Map<String, Set<String>> bfmap = new HashMap<>();
+        bfmap.put("b1", Collections.singleton("g:f1:1"));
+        bfmap.put("b2", Collections.singleton("g:f2:1"));
+
+        Map<String, List<String>> frmap = new HashMap<>();
+        frmap.put("g:f1:1", Arrays.asList("deprecated", "internal"));
+        frmap.put("g:f2:1", Collections.singletonList("internal"));
+        frmap.put("__region.order__", Arrays.asList("global", "deprecated", "internal"));
+
+        Map<String, Set<String>> rpmap = new HashMap<>();
+        rpmap.put("internal", Collections.singleton("xyz"));
+        rpmap.put("deprecated", Collections.singleton("abc"));
+
+        ResolverHookImpl rh = new ResolverHookImpl(new RegionConfiguration(bsnvermap, bfmap, frmap, rpmap, Collections.singleton("global")));
+
+        // b2 needs to resolve to the "abc" package, which is exported into the 'deprecated' region.
+        // f2 doesn't directly see the 'deprecated' region (only internal), but since it's declared before
+        // the internal region by f1, f2 implicitly gets visibility of it because it comes before the
+        // internal region in the global region ordering.
+        BundleRequirement req1 = mockRequirement("b2", bsnvermap);
+        BundleCapability cap1 = mockCapability("abc", "b1", bsnvermap);
+        List<BundleCapability> candidates1 = new ArrayList<>(Arrays.asList(cap1));
+        rh.filterMatches(req1, candidates1);
+
+        assertEquals(Collections.singletonList(cap1), candidates1);
+    }
+
+    @Test
+    public void testEmptyCandidates() {
+        Map<Entry<String, Version>, List<String>> bsnvermap = new HashMap<>();
+        bsnvermap.put(new AbstractMap.SimpleEntry<String,Version>(
+                "b1", new Version(1,0,0)), Collections.singletonList("b1"));
+        Map<String, Set<String>> bfmap = new HashMap<>();
+        Map<String, List<String>> frmap = new HashMap<>();
+        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);
+        List<BundleCapability> candidates1 = new ArrayList<>();
+        rh.filterMatches(req1, candidates1);
+        assertEquals("There were no candidates, there still are none", 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/features1.properties b/src/test/resources/features1.properties
index 9efad8d..0bb6f89 100644
--- a/src/test/resources/features1.properties
+++ b/src/test/resources/features1.properties
@@ -1,4 +1,5 @@
 #Generated at Sat Nov 03 11:10:29 GMT 2018
 #Sat Nov 03 11:10:29 GMT 2018
 an.other\:feature\:123=global
-org.sling\:something\:1.2.3=internal,global
+org.sling\:something\:1.2.3=global,internal
+__region.order__=global,internal
\ No newline at end of file
diff --git a/src/test/resources/props1/features.properties b/src/test/resources/props1/features.properties
index 9efad8d..0bb6f89 100644
--- a/src/test/resources/props1/features.properties
+++ b/src/test/resources/props1/features.properties
@@ -1,4 +1,5 @@
 #Generated at Sat Nov 03 11:10:29 GMT 2018
 #Sat Nov 03 11:10:29 GMT 2018
 an.other\:feature\:123=global
-org.sling\:something\:1.2.3=internal,global
+org.sling\:something\:1.2.3=global,internal
+__region.order__=global,internal
\ No newline at end of file
diff --git a/src/test/resources/props2/features.properties b/src/test/resources/props2/features.properties
index c2cb887..4597034 100644
--- a/src/test/resources/props2/features.properties
+++ b/src/test/resources/props2/features.properties
@@ -1,3 +1,4 @@
 #Generated at Sat Nov 03 11:10:29 GMT 2018
 #Sat Nov 03 11:10:29 GMT 2018
 org.sling\:something\:1.2.3=r0,r1,r2,r3
+__region.order__=global,r0,r1,r2,r3
\ No newline at end of file
diff --git a/src/test/resources/props3/features.properties b/src/test/resources/props3/features.properties
index 9efad8d..0bb6f89 100644
--- a/src/test/resources/props3/features.properties
+++ b/src/test/resources/props3/features.properties
@@ -1,4 +1,5 @@
 #Generated at Sat Nov 03 11:10:29 GMT 2018
 #Sat Nov 03 11:10:29 GMT 2018
 an.other\:feature\:123=global
-org.sling\:something\:1.2.3=internal,global
+org.sling\:something\:1.2.3=global,internal
+__region.order__=global,internal
\ No newline at end of file
diff --git a/src/test/resources/props4/features.properties b/src/test/resources/props4/features.properties
index 9a47d1a..8bb06a1 100644
--- a/src/test/resources/props4/features.properties
+++ b/src/test/resources/props4/features.properties
@@ -1,3 +1,4 @@
 #Generated at Sat Nov 03 11:10:29 GMT 2018
 #Sat Nov 03 11:10:29 GMT 2018
-org.sling\:something\:1.2.3=internal,global
+org.sling\:something\:1.2.3=global,internal
+__region.order__=global,internal
\ No newline at end of file