Implement framework property validation
diff --git a/pom.xml b/pom.xml
index d8e03c0..5b2e7a0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,7 +56,7 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.feature</artifactId>
-            <version>1.2.14</version>
+            <version>1.2.15-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidationResult.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidationResult.java
index 09def1a..d757845 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidationResult.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidationResult.java
@@ -26,6 +26,8 @@
 
     private final Map<String, ConfigurationValidationResult> configurationResults = new HashMap<>();
 
+    private final Map<String, PropertyValidationResult> frameworkPropertyResults = new HashMap<>();
+
     /**
      * Is the configuration of the feature valid?
      * @return {@code true} if it is valid
@@ -38,6 +40,14 @@
                 break;
             }
         }
+        if ( valid ) {
+            for(final PropertyValidationResult r : this.frameworkPropertyResults.values()) {
+                if ( !r.isValid() ) {
+                    valid = false;
+                    break;
+                }
+            }
+        }
         return valid;
     }
 
@@ -48,4 +58,12 @@
     public Map<String, ConfigurationValidationResult> getConfigurationResults() {
         return this.configurationResults;
     }
+
+    /**
+     * Get the framework property validation results
+     * @return The results keyed by framework property name
+     */
+    public Map<String, PropertyValidationResult> getFrameworkPropertyResults() {
+        return this.frameworkPropertyResults;
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidator.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidator.java
index 95df5db..bdc19b0 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidator.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidator.java
@@ -25,6 +25,7 @@
 import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationApi;
 import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationDescription;
 import org.apache.sling.feature.extension.apiregions.api.config.FactoryConfigurationDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.FrameworkPropertyDescription;
 import org.apache.sling.feature.extension.apiregions.api.config.Operation;
 import org.apache.sling.feature.extension.apiregions.api.config.Region;
 
@@ -35,6 +36,8 @@
     
     private final ConfigurationValidator configurationValidator = new ConfigurationValidator();
 
+    private final PropertyValidator propertyValidator = new PropertyValidator();
+
     private volatile FeatureProvider featureProvider;
 
     /**
@@ -111,9 +114,32 @@
                     } 
                 }    
             }
+
             // make sure a result exists
             result.getConfigurationResults().computeIfAbsent(config.getPid(), id -> new ConfigurationValidationResult());
         }
+
+        for(final String frameworkProperty : feature.getFrameworkProperties().keySet()) {
+            final RegionInfo regionInfo = getRegionInfo(feature, frameworkProperty);
+            if ( regionInfo == null ) {
+                final PropertyValidationResult pvr = new PropertyValidationResult();
+                pvr.getErrors().add("Unable to properly validate framework property, region info cannot be determined");
+                result.getFrameworkPropertyResults().put(frameworkProperty, pvr);
+            } else {
+                final FrameworkPropertyDescription fpd = api.getFrameworkPropertyDescriptions().get(frameworkProperty);
+                if ( fpd != null ) {
+                    final PropertyValidationResult pvr = propertyValidator.validate(feature.getFrameworkProperties().get(frameworkProperty), fpd);
+                    result.getFrameworkPropertyResults().put(frameworkProperty, pvr);
+                } else if ( regionInfo.region != Region.INTERNAL && api.getInternalFrameworkProperties().contains(frameworkProperty) ) {
+                    final PropertyValidationResult pvr = new PropertyValidationResult();
+                    pvr.getErrors().add("Framework property is not allowed");
+                    result.getFrameworkPropertyResults().put(frameworkProperty, pvr);
+                }
+            } 
+            // make sure a result exists
+            result.getFrameworkPropertyResults().computeIfAbsent(frameworkProperty, id -> new PropertyValidationResult());
+        }
+
         return result;
     }
 
@@ -155,4 +181,32 @@
         }
         return result;
     }
+
+    RegionInfo getRegionInfo(final Feature feature, final String frameworkProperty) {
+        final FeatureProvider provider = this.getFeatureProvider();
+        
+        final List<ArtifactId> list = feature.getFeatureOrigins(feature.getFrameworkPropertyMetadata(frameworkProperty));
+        boolean global = false;
+        for(final ArtifactId id : list) {
+            Feature found = null;
+            if ( feature.getId().equals(id) ) {
+                found = feature;
+            } else {
+                found = provider == null ? null : provider.provide(id);
+            }
+            if ( found == null ) {
+                return null;
+            }
+            final ConfigurationApi api = ConfigurationApi.getConfigurationApi(found);
+            if ( api == null || api.getRegion() != Region.INTERNAL ) {
+                global = true;
+                break;
+            }
+        }
+        final RegionInfo result = new RegionInfo();
+        result.region = global ? Region.GLOBAL : Region.INTERNAL;
+        result.isUpdate = list.size() > 1;
+
+        return result;
+    }
 }
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidatorTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidatorTest.java
index fb4fa0c..8d4130a 100644
--- a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidatorTest.java
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidatorTest.java
@@ -31,8 +31,10 @@
 import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationApi;
 import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationDescription;
 import org.apache.sling.feature.extension.apiregions.api.config.FactoryConfigurationDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.FrameworkPropertyDescription;
 import org.apache.sling.feature.extension.apiregions.api.config.Operation;
 import org.apache.sling.feature.extension.apiregions.api.config.PropertyDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.PropertyType;
 import org.apache.sling.feature.extension.apiregions.api.config.Region;
 import org.junit.Before;
 import org.junit.Test;
@@ -59,6 +61,8 @@
         fc.getProperties().put("key", "value");
         f.getConfigurations().add(fc);
 
+        f.getFrameworkProperties().put("prop", "1");
+
         return f;
     }
 
@@ -75,10 +79,14 @@
 
         api.getFactoryConfigurationDescriptions().put(FACTORY_PID, fd);
 
+        final FrameworkPropertyDescription fpd = new FrameworkPropertyDescription();
+        fpd.setType(PropertyType.INTEGER);
+        api.getFrameworkPropertyDescriptions().put("prop", fpd);
+
         return api;
     }
 
-    @Test public void testGetRegionInfoNoOrigin() {
+    @Test public void testGetRegionInfoConfigurationNoOrigin() {
         final Feature f1 = createFeature("g:a:1");
         final Configuration cfg = f1.getConfigurations().getConfiguration(PID);
 
@@ -109,7 +117,7 @@
         assertFalse(info.isUpdate);
     }
      
-    @Test public void testGetRegionInfoSingleOrigin() {
+    @Test public void testGetRegionInfoConfigurationSingleOrigin() {
         final Feature f1 = createFeature("g:a:1");
         final Configuration cfg = f1.getConfigurations().getConfiguration(PID);
 
@@ -151,7 +159,7 @@
         assertNull(info);
     }
 
-    @Test public void testGetRegionInfoMultipleOrigins() {
+    @Test public void testGetRegionInfoConfigurationMultipleOrigins() {
         final Feature f1 = createFeature("g:a:1");
         final Configuration cfg = f1.getConfigurations().getConfiguration(PID);
 
@@ -224,7 +232,152 @@
         assertEquals(Region.GLOBAL, info.region);
         assertTrue(info.isUpdate);
     }
-    @Test public void testSingleValidation() {
+
+    @Test public void testGetRegionInfoFrameworkPropertyNoOrigin() {
+        final Feature f1 = createFeature("g:a:1");
+
+        // no api set
+        FeatureValidator.RegionInfo info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.GLOBAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // empty region in api
+        final ConfigurationApi api = createApi();
+        ConfigurationApi.setConfigurationApi(f1, api);
+        info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.GLOBAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // global region in api
+        api.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.GLOBAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // internal region in api
+        api.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.INTERNAL, info.region);
+        assertFalse(info.isUpdate);
+    }
+     
+    @Test public void testGetRegionInfoFrameworkPropertySingleOrigin() {
+        final Feature f1 = createFeature("g:a:1");
+
+        final Feature f2 = createFeature("g:b:1");
+        f1.setFeatureOrigins(f1.getFrameworkPropertyMetadata("prop"), Collections.singletonList(f2.getId()));
+
+        // set feature provider to always provide f2
+        this.validator.setFeatureProvider(id -> f2);
+        // no api in origin
+        FeatureValidator.RegionInfo info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.GLOBAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // no region in api
+        final ConfigurationApi api2 = new ConfigurationApi();
+        ConfigurationApi.setConfigurationApi(f2, api2);
+        info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.GLOBAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // global in api
+        api2.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(f2, api2);
+        info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.GLOBAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // internal in api
+        api2.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(f2, api2);
+        info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.INTERNAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // unknown id
+        this.validator.setFeatureProvider(id -> null);
+        f1.setFeatureOrigins(f1.getFrameworkPropertyMetadata("prop"), Collections.singletonList(ArtifactId.parse("g:xy:1")));
+        info = validator.getRegionInfo(f1, "prop");
+        assertNull(info);
+    }
+
+    @Test public void testGetRegionInfoFrameworkPropertyMultipleOrigins() {
+        final Feature f1 = createFeature("g:a:1");
+
+        final Feature f2 = createFeature("g:b:1");
+        final Feature f3 = createFeature("g:c:1");
+        f1.setFeatureOrigins(f1.getFrameworkPropertyMetadata("prop"), Arrays.asList(f2.getId(), f3.getId()));
+
+        final FeatureProvider provider = new FeatureProvider() {
+
+			@Override
+			public Feature provide(final ArtifactId id) {
+                if ( f1.getId().equals(id) ) {
+                    return f1;
+                } else if ( f2.getId().equals(id)) {
+                    return f2;
+                } else if ( f3.getId().equals(id)) {
+                    return f3;
+                }
+				return null;
+			}
+            
+        };
+
+        this.validator.setFeatureProvider(provider);
+
+        // no api in origins
+        FeatureValidator.RegionInfo info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.GLOBAL, info.region);
+        assertTrue(info.isUpdate);
+
+        // global-internal
+        final ConfigurationApi api2 = new ConfigurationApi();
+        final ConfigurationApi api3 = new ConfigurationApi();
+        api2.setRegion(Region.GLOBAL);
+        api3.setRegion(Region.INTERNAL);        
+        ConfigurationApi.setConfigurationApi(f2, api2);
+        ConfigurationApi.setConfigurationApi(f3, api3);
+
+        info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.GLOBAL, info.region);
+        assertTrue(info.isUpdate);
+
+        // global-global
+        api2.setRegion(Region.GLOBAL);
+        api3.setRegion(Region.GLOBAL);        
+        ConfigurationApi.setConfigurationApi(f2, api2);
+        ConfigurationApi.setConfigurationApi(f3, api3);
+
+        info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.GLOBAL, info.region);
+        assertTrue(info.isUpdate);
+
+        // internal-internal
+        api2.setRegion(Region.INTERNAL);
+        api3.setRegion(Region.INTERNAL);        
+        ConfigurationApi.setConfigurationApi(f2, api2);
+        ConfigurationApi.setConfigurationApi(f3, api3);
+
+        info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.INTERNAL, info.region);
+        assertTrue(info.isUpdate);
+
+        // internal-global
+        api2.setRegion(Region.INTERNAL);
+        api3.setRegion(Region.GLOBAL);        
+        ConfigurationApi.setConfigurationApi(f2, api2);
+        ConfigurationApi.setConfigurationApi(f3, api3);
+
+        info = validator.getRegionInfo(f1, "prop");
+        assertEquals(Region.GLOBAL, info.region);
+        assertTrue(info.isUpdate);
+    }
+
+    @Test public void testSingleConfigurationValidation() {
         final Feature f1 = createFeature("g:a:1");
         final ConfigurationApi api = createApi();
         ConfigurationApi.setConfigurationApi(f1, api);
@@ -433,4 +586,50 @@
         result = validator.validate(f1, api);
         assertTrue(result.isValid());
     }
+
+    @Test public void testInternalFrameworkProperty() {
+        final Feature f1 = createFeature("g:a:1");
+        final ConfigurationApi api = new ConfigurationApi();
+        ConfigurationApi.setConfigurationApi(f1, api);
+
+        // global region
+        FeatureValidationResult result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        // mark framework property as internal
+        api.getInternalFrameworkProperties().add("prop");
+        ConfigurationApi.setConfigurationApi(f1, api);
+
+        // global region
+        result = validator.validate(f1, api);
+        assertFalse(result.isValid());
+        assertFalse(result.getFrameworkPropertyResults().get("prop").isValid());
+
+        // internal region
+        api.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+    }
+
+    @Test public void testFrameworkProperty() {
+        final Feature f1 = createFeature("g:a:1");
+        final ConfigurationApi api = createApi();
+        ConfigurationApi.setConfigurationApi(f1, api);
+
+        // value is valid
+        FeatureValidationResult result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        // no value -> valid
+        f1.getFrameworkProperties().remove("prop");
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        // invalid value
+        f1.getFrameworkProperties().put("prop", "foo");
+        result = validator.validate(f1, api);
+        assertFalse(result.isValid());
+        assertFalse(result.getFrameworkPropertyResults().get("prop").isValid());
+    }
 }