Merge branch 'master' into SLING-9867
diff --git a/.gitignore b/.gitignore
index 5b783ed..38f5ca4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,5 @@
 .vlt
 .DS_Store
 jcr.log
+.vscode
 atlassian-ide-plugin.xml
diff --git a/pom.xml b/pom.xml
index 7bc0bec..a980994 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/ConfigurationApiMergeHandler.java b/src/main/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandler.java
new file mode 100644
index 0000000..b7d6b68
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandler.java
@@ -0,0 +1,100 @@
+/*
+ * 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.sling.feature.extension.apiregions;
+
+import java.util.Map;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.builder.HandlerContext;
+import org.apache.sling.feature.builder.MergeHandler;
+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.Region;
+
+/**
+ * Merge the configuration api extension
+ */
+public class ConfigurationApiMergeHandler implements MergeHandler {
+
+    @Override
+    public boolean canMerge(final Extension extension) {
+        return ConfigurationApi.EXTENSION_NAME.equals(extension.getName());
+    }
+
+    @Override
+    public void merge(final HandlerContext context, 
+        final Feature targetFeature, 
+        final Feature sourceFeature, 
+        final Extension targetExtension, 
+        final Extension sourceExtension) {
+
+        if ( targetExtension == null ) {
+            // no target available yet, just copy source
+            final Extension ext = new Extension(ExtensionType.JSON, ConfigurationApi.EXTENSION_NAME, sourceExtension.getState());
+            ext.setJSON(sourceExtension.getJSON());
+            targetFeature.getExtensions().add(ext);
+        } else {
+            final ConfigurationApi sourceApi = ConfigurationApi.getConfigurationApi(sourceExtension);
+            final ConfigurationApi targetApi = ConfigurationApi.getConfigurationApi(targetExtension);
+
+            // region merging
+            if ( context.isInitialMerge() ) {
+                targetApi.setRegion(sourceApi.getRegion());
+            } else {
+                // region merging is different for prototypes
+                if ( sourceApi.getRegion() != targetApi.getRegion() ) {
+                    if ( context.isPrototypeMerge() ) {
+                        if ( sourceApi.getRegion() != null ) {
+                            targetApi.setRegion(sourceApi.getRegion());
+                        }
+                    } else {                    
+                        targetApi.setRegion(Region.GLOBAL);
+                    }
+                }
+            }
+
+            // merge - but throw on duplicates
+            for(final Map.Entry<String, ConfigurationDescription> entry : sourceApi.getConfigurationDescriptions().entrySet()) {
+                if ( targetApi.getConfigurationDescriptions().containsKey(entry.getKey())) {
+                    throw new IllegalStateException("Duplicate configuration description " + entry.getKey());
+                }
+                targetApi.getConfigurationDescriptions().put(entry.getKey(), entry.getValue());
+            }
+            for(final Map.Entry<String, FactoryConfigurationDescription> entry : sourceApi.getFactoryConfigurationDescriptions().entrySet()) {
+                if ( targetApi.getFactoryConfigurationDescriptions().containsKey(entry.getKey())) {
+                    throw new IllegalStateException("Duplicate factory configuration description " + entry.getKey());
+                }
+                targetApi.getFactoryConfigurationDescriptions().put(entry.getKey(), entry.getValue());
+            }
+            for(final Map.Entry<String, FrameworkPropertyDescription> entry : sourceApi.getFrameworkPropertyDescriptions().entrySet()) {
+                if ( targetApi.getFrameworkPropertyDescriptions().containsKey(entry.getKey())) {
+                    throw new IllegalStateException("Duplicate framework property description " + entry.getKey());
+                }
+                targetApi.getFrameworkPropertyDescriptions().put(entry.getKey(), entry.getValue());
+            }
+            targetApi.getInternalConfigurations().addAll(sourceApi.getInternalConfigurations());
+            targetApi.getInternalFactoryConfigurations().addAll(sourceApi.getInternalFactoryConfigurations());
+            targetApi.getInternalFrameworkProperties().addAll(sourceApi.getInternalFrameworkProperties());
+
+            ConfigurationApi.setConfigurationApi(targetFeature, targetApi);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckConfigurationApi.java b/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckConfigurationApi.java
new file mode 100644
index 0000000..20a5d6c
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/analyser/CheckConfigurationApi.java
@@ -0,0 +1,88 @@
+/*
+ * 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.sling.feature.extension.apiregions.analyser;
+
+import java.util.Map;
+
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationApi;
+import org.apache.sling.feature.extension.apiregions.api.config.validation.ConfigurationValidationResult;
+import org.apache.sling.feature.extension.apiregions.api.config.validation.FeatureValidationResult;
+import org.apache.sling.feature.extension.apiregions.api.config.validation.FeatureValidator;
+import org.apache.sling.feature.extension.apiregions.api.config.validation.PropertyValidationResult;
+
+
+public class CheckConfigurationApi implements AnalyserTask{
+
+    @Override
+    public String getId() {
+        return "configuration-api";
+    }
+
+    @Override
+    public String getName() {
+        return "Configuration API analyser task";
+    }
+
+	@Override
+	public void execute(final AnalyserTaskContext context) throws Exception {
+        final FeatureValidator validator = new FeatureValidator();
+        validator.setFeatureProvider(context.getFeatureProvider());
+        
+        final ConfigurationApi api = ConfigurationApi.getConfigurationApi(context.getFeature());
+        if ( api == null ) {
+            context.reportExtensionWarning(ConfigurationApi.EXTENSION_NAME, "Configuration api is not specified, unable to validate feature");
+        } else {
+            final FeatureValidationResult result = validator.validate(context.getFeature(), api);
+            if ( !result.isValid() ) {
+                for(final Map.Entry<String, PropertyValidationResult> entry : result.getFrameworkPropertyResults().entrySet()) {
+                    for(final String warn : entry.getValue().getWarnings()) {
+                        context.reportWarning("Framework property " + entry.getKey() + " : " + warn);
+                    }
+                    if ( !entry.getValue().isValid() ) {
+                        for(final String err : entry.getValue().getErrors()) {
+                            context.reportError("Framework property " + entry.getKey() + " : " + err);
+                        }
+                    }
+                }
+                for(final Map.Entry<String, ConfigurationValidationResult> entry : result.getConfigurationResults().entrySet()) {
+                    for(final String warn : entry.getValue().getWarnings()) {
+                        context.reportWarning("Configuration " + entry.getKey() + " : " + warn);
+                    }
+                    for(final Map.Entry<String, PropertyValidationResult> propEntry : entry.getValue().getPropertyResults().entrySet()) {
+                        for(final String warn : propEntry.getValue().getWarnings()) {
+                            context.reportWarning("Configuration " + entry.getKey() + "." + propEntry.getKey() + " : " + warn);
+                        }             
+                    }
+                    if ( !entry.getValue().isValid() ) {
+                        for(final String err : entry.getValue().getGlobalErrors()) {
+                            context.reportError("Configuration " + entry.getKey() + " : " + err);
+                        }
+                        for(final Map.Entry<String, PropertyValidationResult> propEntry : entry.getValue().getPropertyResults().entrySet()) {
+                            if ( !propEntry.getValue().isValid() ) {
+                                for(final String err : propEntry.getValue().getErrors()) {
+                                    context.reportWarning("Configuration " + entry.getKey() + "." + propEntry.getKey() + " : " + err);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+	}
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntity.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntity.java
new file mode 100644
index 0000000..31210f8
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntity.java
@@ -0,0 +1,187 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+
+import org.apache.felix.cm.json.Configurations;
+
+/**
+ * Abstract class used by all entities which allow additional attributes to be stored.
+ */
+public abstract class AttributeableEntity {
+	
+	/** The additional attributes */
+	private final Map<String, JsonValue> attributes = new LinkedHashMap<>();
+
+    /**
+     * Clear the object and reset to defaults
+     */
+	public void clear() {
+		this.attributes.clear();
+	}
+	
+   /**
+     * Convert this object into JSON
+     *
+     * @return The json object
+     * @throws IOException If generating the JSON fails
+     */
+    public JsonObject toJSONObject() throws IOException {
+        final JsonObjectBuilder objectBuilder = this.createJson();
+        return objectBuilder.build();
+    }
+
+	/**
+	 * Extract the metadata from the JSON object.
+	 * This method first calls {@link #clear()}
+     * 
+	 * @param jsonObj The JSON Object
+	 * @throws IOException If JSON parsing fails
+	 */
+	public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+		this.clear();
+        try {
+            for(final Map.Entry<String, JsonValue> entry : jsonObj.entrySet()) {
+				this.getAttributes().put(entry.getKey(), entry.getValue());
+			}
+        } catch (final JsonException | IllegalArgumentException e) {
+            throw new IOException(e);
+		}
+	}
+
+	/**
+	 * Get the attributes
+	 * @return Mutable map of attributes, by attribute name
+	 */
+	public Map<String, JsonValue> getAttributes() {
+        return this.attributes;
+    }
+
+	/**
+     * Convert this object into JSON
+     *
+     * @return The json object builder
+     * @throws IOException If generating the JSON fails
+     */
+    JsonObjectBuilder createJson() throws IOException {
+		final JsonObjectBuilder objectBuilder = Json.createObjectBuilder();
+
+		for(final Map.Entry<String, JsonValue> entry : this.getAttributes().entrySet()) {
+			objectBuilder.add(entry.getKey(), entry.getValue());
+		}
+
+		return objectBuilder;
+	}
+	
+	/**
+	 * Helper method to get a string value from a JsonValue
+	 * @param jsonValue The json value
+	 * @return The string value or {@code null}.
+	 */
+	String getString(final JsonValue jsonValue) {
+		final Object obj = Configurations.convertToObject(jsonValue);
+		if ( obj != null ) {
+			return obj.toString();
+		}
+		return null;
+	}
+
+	/**
+	 * Helper method to get a string value from an attribute
+	 * @param attributeName The attribute name
+	 * @return The string value or {@code null}.
+	 */
+	String getString(final String attributeName) {
+		final JsonValue val = this.getAttributes().remove(attributeName);
+		if ( val != null ) {
+			final Object obj = Configurations.convertToObject(val);
+			if ( obj != null ) {
+				return obj.toString();
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Helper method to get a number value from an attribute
+	 * @param attributeName The attribute name
+	 * @return The string value or {@code null}.
+     * @throws IOException If the attribute value is not of type boolean
+	 */
+	Number getNumber(final String attributeName) throws IOException {
+		final JsonValue val = this.getAttributes().remove(attributeName);
+		if ( val != null ) {
+			final Object obj = Configurations.convertToObject(val);
+			if ( obj instanceof Number ) {
+				return (Number)obj;
+			}
+			throw new IOException("Invalid type for number value " + attributeName + " : " + val.getValueType().name());
+		}
+		return null;
+	}
+
+    /**
+     * Helper method to set a string value
+     */
+    void setString(final JsonObjectBuilder builder, final String attributeName, final String value) {
+		if ( value != null ) {
+			builder.add(attributeName, value);
+		}
+	}
+
+	/**
+	 * Helper method to get a integer value from an attribute
+	 * @param attributeName The attribute name
+	 * @param defaultValue default value
+	 * @return The integer value or the default value
+	 */
+	int getInteger(final String attributeName, final int defaultValue) {
+		final String val = this.getString(attributeName);
+		if ( val != null ) {
+			return Integer.parseInt(val);
+		}
+		return defaultValue;
+	}
+
+	/**
+	 * Helper method to get a boolean value from an attribute
+	 * @param attributeName The attribute name
+	 * @param defaultValue default value
+	 * @return The boolean value or the default value
+     * @throws IOException If the attribute value is not of type boolean
+	 */
+	boolean getBoolean(final String attributeName, final boolean defaultValue) throws IOException {
+		final JsonValue val = this.getAttributes().remove(attributeName);
+		if ( val != null ) {
+			final Object obj = Configurations.convertToObject(val);
+			if ( obj instanceof Boolean ) {
+                return ((Boolean)obj).booleanValue();
+			}
+			throw new IOException("Invalid type for boolean value " + attributeName + " : " + val.getValueType().name());
+		}
+		return defaultValue;
+	}
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntity.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntity.java
new file mode 100644
index 0000000..6f25d4d
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntity.java
@@ -0,0 +1,95 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+
+/** 
+ * A configurable entity has properties
+ */
+public abstract class ConfigurableEntity extends DescribableEntity {
+	
+	/** The properties */
+    private final Map<String, PropertyDescription> properties = new LinkedHashMap<>();
+
+    /**
+     * Clear the object and reset to defaults
+     */
+	public void clear() {
+        super.clear();
+		this.properties.clear();
+    }
+
+	/**
+	 * Extract the metadata from the JSON object.
+	 * This method first calls {@link #clear()}
+     * 
+	 * @param jsonObj The JSON Object
+	 * @throws IOException If JSON parsing fails
+	 */
+	public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+        super.fromJSONObject(jsonObj);
+        try {
+            final JsonValue val = this.getAttributes().remove(InternalConstants.KEY_PROPERTIES);
+            if ( val != null ) {
+                for(final Map.Entry<String, JsonValue> innerEntry : val.asJsonObject().entrySet()) {
+					final PropertyDescription prop = new PropertyDescription();
+					prop.fromJSONObject(innerEntry.getValue().asJsonObject());
+                    this.getPropertyDescriptions().put(innerEntry.getKey(), prop);
+                }
+            }            
+        } catch (final JsonException | IllegalArgumentException e) {
+            throw new IOException(e);
+        }
+	}
+
+	/**
+	 * Get the properties
+	 * @return Mutable map of properties by property name
+	 */
+    public Map<String, PropertyDescription> getPropertyDescriptions() {
+        return this.properties;
+    }
+
+    /**
+     * Convert this object into JSON
+     *
+     * @return The json object builder
+     * @throws IOException If generating the JSON fails
+     */
+	JsonObjectBuilder createJson() throws IOException {
+		final JsonObjectBuilder objBuilder = super.createJson();
+
+		if ( !this.getPropertyDescriptions().isEmpty() ) {
+			final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+			for(final Map.Entry<String, PropertyDescription> entry : this.getPropertyDescriptions().entrySet()) {
+				propBuilder.add(entry.getKey(), entry.getValue().createJson());
+			}
+			objBuilder.add(InternalConstants.KEY_PROPERTIES, propBuilder);
+		}
+
+		return objBuilder;
+   }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApi.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApi.java
new file mode 100644
index 0000000..2d7af47
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApi.java
@@ -0,0 +1,332 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+
+/**
+ * A configuration api describes the set of supported OSGi
+ * configurations and framework properties. This object can be
+ * stored as an extension inside a feature model.
+ */
+public class ConfigurationApi extends AttributeableEntity {
+    
+    /** The name of the api regions extension. */
+    public static final String EXTENSION_NAME = "configuration-api";
+  
+    /**
+     * Get the configuration api from the feature - if it exists.
+     * 
+     * @param feature The feature
+     * @return The configuration api or {@code null}.
+     * @throws IllegalArgumentException If the extension is wrongly formatted
+     */
+    public static ConfigurationApi getConfigurationApi(final Feature feature) {
+        final Extension ext = feature == null ? null : feature.getExtensions().getByName(EXTENSION_NAME);
+        return getConfigurationApi(ext);
+    }
+
+    /**
+     * Get the configuration api from the extension.
+     * 
+     * @param ext The extension
+     * @return The configuration api or {@code null} if the extension is {@code null}.
+     * @throws IllegalArgumentException If the extension is wrongly formatted
+     */
+    public static ConfigurationApi getConfigurationApi(final Extension ext) {
+        if ( ext == null ) {
+            return null;
+        }
+        if ( ext.getType() != ExtensionType.JSON ) {
+            throw new IllegalArgumentException("Extension " + ext.getName() + " must have JSON type");
+        }
+        try {
+            final ConfigurationApi result = new ConfigurationApi();
+            result.fromJSONObject(ext.getJSONStructure().asJsonObject());
+            return result;
+        } catch ( final IOException ioe) {
+            throw new IllegalArgumentException(ioe.getMessage(), ioe);
+        }
+    }
+   
+    /**
+     * Set the configuration api as an extension to the feature
+     * @param feature The feature
+     * @param api The configuration api
+     * @throws IllegalStateException If the feature has already an extension of a wrong type
+     * @throws IllegalArgumentException If the api configuration can't be serialized to JSON
+     */
+    public static void setConfigurationApi(final Feature feature, final ConfigurationApi api) {
+        Extension ext = feature.getExtensions().getByName(EXTENSION_NAME);
+        if ( api == null ) {
+            if ( ext != null ) {
+                feature.getExtensions().remove(ext);
+            }
+        } else {
+            if ( ext == null ) {
+                ext = new Extension(ExtensionType.JSON, EXTENSION_NAME, ExtensionState.OPTIONAL);
+                feature.getExtensions().add(ext);
+            }
+            try {
+                ext.setJSONStructure(api.toJSONObject());
+            } catch ( final IOException ioe) {
+                throw new IllegalArgumentException(ioe);
+            }
+        }
+    }
+
+    /** The map of configurations */
+ 	private final Map<String, ConfigurationDescription> configurations = new LinkedHashMap<>();
+
+    /** The map of factory configurations */
+    private final Map<String, FactoryConfigurationDescription> factories = new LinkedHashMap<>();
+
+    /** The map of framework properties */
+    private final Map<String, FrameworkPropertyDescription> frameworkProperties = new LinkedHashMap<>();
+
+    /** The list of internal configuration names */
+    private final List<String> internalConfigurations = new ArrayList<>();
+
+    /** The list of internal factory configuration names */
+    private final List<String> internalFactories = new ArrayList<>();
+
+    /** The list of internal framework property names */
+    private final List<String> internalFrameworkProperties = new ArrayList<>();
+    
+    /** The configuration region of this feature */
+    private Region region;
+
+    /**
+     * Clear the object and reset to defaults
+     */
+    public void clear() {
+        super.clear();
+        this.configurations.clear();
+        this.factories.clear();
+        this.frameworkProperties.clear();
+        this.internalConfigurations.clear();
+        this.internalFactories.clear();
+        this.internalFrameworkProperties.clear();
+        this.setRegion(null);
+    }
+
+	/**
+	 * Extract the metadata from the JSON object.
+	 * This method first calls {@link #clear()}.
+     * 
+	 * @param jsonObj The JSON Object
+	 * @throws IOException If JSON parsing fails
+	 */
+    public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+        super.fromJSONObject(jsonObj);
+        try {
+			final String typeVal = this.getString(InternalConstants.KEY_REGION);
+			if ( typeVal != null ) {
+                this.setRegion(Region.valueOf(typeVal.toUpperCase()));				
+			}
+
+            JsonValue val;
+            val = this.getAttributes().remove(InternalConstants.KEY_CONFIGURATIONS);
+            if ( val != null ) {
+                for(final Map.Entry<String, JsonValue> innerEntry : val.asJsonObject().entrySet()) {
+                    final ConfigurationDescription cfg = new ConfigurationDescription();
+                    cfg.fromJSONObject(innerEntry.getValue().asJsonObject());
+                    this.getConfigurationDescriptions().put(innerEntry.getKey(), cfg);
+                }
+            }
+            
+            val = this.getAttributes().remove(InternalConstants.KEY_FACTORIES);
+            if ( val != null ) {
+                for(final Map.Entry<String, JsonValue> innerEntry : val.asJsonObject().entrySet()) {
+                    final FactoryConfigurationDescription cfg = new FactoryConfigurationDescription();
+                    cfg.fromJSONObject(innerEntry.getValue().asJsonObject());
+                    this.getFactoryConfigurationDescriptions().put(innerEntry.getKey(), cfg);
+                }
+            }
+
+            val = this.getAttributes().remove(InternalConstants.KEY_FWK_PROPERTIES);
+            if ( val != null ) {
+                for(final Map.Entry<String, JsonValue> innerEntry : val.asJsonObject().entrySet()) {
+                    final FrameworkPropertyDescription cfg = new FrameworkPropertyDescription();
+                    cfg.fromJSONObject(innerEntry.getValue().asJsonObject());
+                    this.getFrameworkPropertyDescriptions().put(innerEntry.getKey(), cfg);
+                }
+            }
+
+            val = this.getAttributes().remove(InternalConstants.KEY_INTERNAL_CONFIGURATIONS);
+            if ( val != null ) {
+                for(final JsonValue innerVal : val.asJsonArray()) {
+                    this.getInternalConfigurations().add(getString(innerVal));
+                }
+            }
+
+            val = this.getAttributes().remove(InternalConstants.KEY_INTERNAL_FACTORIES);
+            if ( val != null ) {
+                for(final JsonValue innerVal : val.asJsonArray()) {
+                    this.getInternalFactoryConfigurations().add(getString(innerVal));
+                }
+            }
+
+            val = this.getAttributes().remove(InternalConstants.KEY_INTERNAL_FWK_PROPERTIES);
+            if ( val != null ) {
+                for(final JsonValue innerVal : val.asJsonArray()) {
+                    this.getInternalFrameworkProperties().add(getString(innerVal));
+                }
+            }
+
+        } catch (final JsonException | IllegalArgumentException e) {
+            throw new IOException(e);
+        }
+    }
+
+    /**
+     * Get the configuration descriptions
+	 * @return Mutable map of configuration descriptions by pid
+	 */
+	public Map<String, ConfigurationDescription> getConfigurationDescriptions() {
+		return configurations;
+	}
+
+	/**
+     * Get the factory configuration descriptions
+	 * @return Mutable map of factory descriptions by factory pid
+	 */
+	public Map<String, FactoryConfigurationDescription> getFactoryConfigurationDescriptions() {
+		return factories;
+	}
+
+	/**
+     * Get the framework properties
+	 * @return Mutable map of framework properties
+	 */
+	public Map<String, FrameworkPropertyDescription> getFrameworkPropertyDescriptions() {
+		return frameworkProperties;
+	}
+
+	/**
+     * Get the internal configuration pids
+	 * @return Mutable list of internal configuration pids
+	 */
+	public List<String> getInternalConfigurations() {
+		return internalConfigurations;
+	}
+
+	/**
+     * Get the internal factory pids
+	 * @return Mutable list of internal factory configuration pids
+	 */
+	public List<String> getInternalFactoryConfigurations() {
+		return internalFactories;
+	}
+
+	/**
+     * Get the internal framework property names
+	 * @return Mutable list of internal framework property names
+	 */
+	public List<String> getInternalFrameworkProperties() {
+		return internalFrameworkProperties;
+    }
+
+    /**
+     * Get the api configuration region
+     * @return The region or {@code null}
+     */
+    public Region getRegion() {
+        return this.region;
+    }
+
+    /**
+     * Set the api configuration region
+     * @param value The region to set
+     */
+    public void setRegion(final Region value) {
+        this.region = value;
+    }
+
+    /**
+     * Convert this object into JSON
+     *
+     * @return The json object builder
+     * @throws IOException If generating the JSON fails
+     */
+    JsonObjectBuilder createJson() throws IOException {
+		final JsonObjectBuilder objBuilder = super.createJson();
+        if ( this.getRegion() != null ) {
+            objBuilder.add(InternalConstants.KEY_REGION, this.getRegion().name());
+        }
+        if ( !this.getConfigurationDescriptions().isEmpty() ) {
+            final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+            for(final Map.Entry<String, ConfigurationDescription> entry : this.getConfigurationDescriptions().entrySet()) {
+                propBuilder.add(entry.getKey(), entry.getValue().createJson());
+            }
+            objBuilder.add(InternalConstants.KEY_CONFIGURATIONS, propBuilder);
+        }
+        if ( !this.getFactoryConfigurationDescriptions().isEmpty() ) {
+            final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+            for(final Map.Entry<String, FactoryConfigurationDescription> entry : this.getFactoryConfigurationDescriptions().entrySet()) {
+                propBuilder.add(entry.getKey(), entry.getValue().createJson());
+            }
+            objBuilder.add(InternalConstants.KEY_FACTORIES, propBuilder);
+        }
+        if ( !this.getFrameworkPropertyDescriptions().isEmpty() ) {
+            final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+            for(final Map.Entry<String, FrameworkPropertyDescription> entry : this.getFrameworkPropertyDescriptions().entrySet()) {
+                propBuilder.add(entry.getKey(), entry.getValue().createJson());
+            }
+            objBuilder.add(InternalConstants.KEY_FWK_PROPERTIES, propBuilder);
+        }
+        if ( !this.getInternalConfigurations().isEmpty() ) {
+            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+            for(final String n : this.getInternalConfigurations()) {
+                arrayBuilder.add(n);
+            }
+			objBuilder.add(InternalConstants.KEY_INTERNAL_CONFIGURATIONS, arrayBuilder);
+		}
+		if ( !this.getInternalFactoryConfigurations().isEmpty() ) {
+            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+            for(final String n : this.getInternalFactoryConfigurations()) {
+                arrayBuilder.add(n);
+            }
+			objBuilder.add(InternalConstants.KEY_INTERNAL_FACTORIES, arrayBuilder);
+		}
+		if ( !this.getInternalFrameworkProperties().isEmpty() ) {
+            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+            for(final String n : this.getInternalFrameworkProperties()) {
+                arrayBuilder.add(n);
+            }
+			objBuilder.add(InternalConstants.KEY_INTERNAL_FWK_PROPERTIES, arrayBuilder);
+        }
+
+		return objBuilder;
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationDescription.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationDescription.java
new file mode 100644
index 0000000..0e2c7e9
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationDescription.java
@@ -0,0 +1,24 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+/**
+ * A description of an OSGi configuration
+ */
+public class ConfigurationDescription extends ConfigurableEntity {
+
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntity.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntity.java
new file mode 100644
index 0000000..b2617d4
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntity.java
@@ -0,0 +1,131 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import java.io.IOException;
+
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+
+/**
+ * Abstract class for all describable entities, having an optional title,
+ * description and deprecation info.
+ */
+public abstract class DescribableEntity extends AttributeableEntity {
+	
+	/** The title */
+    private String title;
+
+	/** The description */
+    private String description;
+
+	/** The optional deprecation text */
+	private String deprecated;
+
+	/**
+     * Clear the object and reset to defaults
+     */
+	public void clear() {
+		super.clear();
+		this.setTitle(null);
+		this.setDescription(null);
+		this.setDeprecated(null);
+	}
+	
+	/**
+	 * Extract the metadata from the JSON object.
+	 * This method first calls {@link #clear()}
+     * 
+	 * @param jsonObj The JSON Object
+	 * @throws IOException If JSON parsing fails
+	 */
+	public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+		super.fromJSONObject(jsonObj);
+        try {
+			this.setTitle(this.getString(InternalConstants.KEY_TITLE));
+			this.setDescription(this.getString(InternalConstants.KEY_DESCRIPTION));
+			this.setDeprecated(this.getString(InternalConstants.KEY_DEPRECATED));
+        } catch (final JsonException | IllegalArgumentException e) {
+            throw new IOException(e);
+		}
+	}
+
+	/**
+	 * Get the title
+	 * @return The title or {@code null}
+	 */
+	public String getTitle() {
+		return title;
+	}
+
+	/**
+	 * Set the title
+	 * @param title the title to set
+	 */
+	public void setTitle(final String title) {
+		this.title = title;
+	}
+
+	/**
+	 * Get the description
+	 * @return the description or {@code null}
+	 */
+	public String getDescription() {
+		return description;
+	}
+
+	/**
+	 * Set the description
+	 * @param description the description to set
+	 */
+	public void setDescription(final String description) {
+		this.description = description;
+	}
+
+	/**
+	 * Get the deprecation text
+	 * @return the deprecation text or {@code null}
+	 */
+	public String getDeprecated() {
+		return deprecated;
+	}
+
+	/**
+	 * Set the deprecation text
+	 * @param deprecated the deprecation text to set
+	 */
+	public void setDeprecated(final String deprecated) {
+		this.deprecated = deprecated;
+	}
+
+	/**
+     * Convert this object into JSON
+     *
+     * @return The json object builder
+     * @throws IOException If generating the JSON fails
+     */
+    JsonObjectBuilder createJson() throws IOException {
+		final JsonObjectBuilder objectBuilder = super.createJson();
+
+		this.setString(objectBuilder, InternalConstants.KEY_TITLE, this.getTitle());
+		this.setString(objectBuilder, InternalConstants.KEY_DESCRIPTION, this.getDescription());
+		this.setString(objectBuilder, InternalConstants.KEY_DEPRECATED, this.getDeprecated());
+
+		return objectBuilder;
+	}
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfigurationDescription.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfigurationDescription.java
new file mode 100644
index 0000000..9c8d7fa
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfigurationDescription.java
@@ -0,0 +1,135 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+
+/**
+ * Description of an OSGi factory configuration
+ */
+public class FactoryConfigurationDescription extends ConfigurableEntity {
+    
+    private final Set<Operation> operations = new HashSet<>();
+
+    private final List<String> internalNames = new ArrayList<>();
+
+    public FactoryConfigurationDescription() {
+        this.setDefaults();
+    }
+
+    void setDefaults() {
+        this.getOperations().add(Operation.CREATE);
+        this.getOperations().add(Operation.UPDATE);
+    }
+    
+    /**
+     * Clear the object and set the defaults
+     */
+    public void clear() {
+        super.clear();
+        this.setDefaults();
+        this.internalNames.clear();
+    }
+
+	/**
+	 * Extract the metadata from the JSON object.
+	 * This method first calls {@link #clear()}
+     *
+	 * @param jsonObj The JSON Object
+	 * @throws IOException If JSON parsing fails
+	 */
+    public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+        super.fromJSONObject(jsonObj);
+        try {
+            JsonValue val;
+            val = this.getAttributes().remove(InternalConstants.KEY_OPERATIONS);
+            if ( val != null ) {
+                this.getOperations().clear();
+                for(final JsonValue innerVal : val.asJsonArray()) {
+                    final String v = getString(innerVal).toUpperCase();
+                    this.getOperations().add(Operation.valueOf(v));
+                }
+                if ( this.getOperations().isEmpty() ) {
+                    throw new IOException("Operations must not be empty");
+                }
+            }
+            
+            val = this.getAttributes().remove(InternalConstants.KEY_INTERNAL_NAMES);
+            if ( val != null ) {
+                for(final JsonValue innerVal : val.asJsonArray()) {
+                    this.getInternalNames().add(getString(innerVal));
+                }
+            }
+
+		} catch (final JsonException | IllegalArgumentException e) {
+            throw new IOException(e);
+        }
+    }
+
+   /**
+    * Get the operations
+	 * @return Mutable set of operations
+	 */
+	public Set<Operation> getOperations() {
+		return operations;
+	}
+
+	/**
+     * Get the internal factory configuration name
+	 * @return Mutable list of internal names
+	 */
+	public List<String> getInternalNames() {
+		return internalNames;
+	}
+
+   /**
+     * Convert this object into JSON
+     *
+     * @return The json object builder
+     * @throws IOException If generating the JSON fails
+     */
+    JsonObjectBuilder createJson() throws IOException {
+		final JsonObjectBuilder objBuilder = super.createJson();
+		
+		if ( !this.getOperations().isEmpty() && this.getOperations().size() != 2 ) {
+            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+            for(final Operation op : this.getOperations()) {
+                arrayBuilder.add(op.name());
+            }
+			objBuilder.add(InternalConstants.KEY_OPERATIONS, arrayBuilder);
+		}
+		if ( !this.getInternalNames().isEmpty() ) {
+            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+            for(final String n : this.getInternalNames()) {
+                arrayBuilder.add(n);
+            }
+			objBuilder.add(InternalConstants.KEY_INTERNAL_NAMES, arrayBuilder);
+		}
+		return objBuilder;
+   }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FrameworkPropertyDescription.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FrameworkPropertyDescription.java
new file mode 100644
index 0000000..df9a3b0
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FrameworkPropertyDescription.java
@@ -0,0 +1,24 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+/**
+ * A framework property description
+ */
+public class FrameworkPropertyDescription extends PropertyDescription {
+    
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/InternalConstants.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/InternalConstants.java
new file mode 100644
index 0000000..2fff4a3
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/InternalConstants.java
@@ -0,0 +1,73 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+/**
+ * Constants used in this implementation
+ */
+abstract class InternalConstants {
+    
+    static final String KEY_TITLE = "title";
+
+	static final String KEY_DESCRIPTION = "description";
+
+	static final String KEY_DEPRECATED = "deprecated";
+	
+	static final String KEY_PROPERTIES = "properties";
+
+    static final String KEY_CONFIGURATIONS = "configurations";
+
+    static final String KEY_FACTORIES = "factory-configurations";
+    
+    static final String KEY_FWK_PROPERTIES = "framework-properties";
+
+    static final String KEY_INTERNAL_CONFIGURATIONS = "internal-configurations";
+
+    static final String KEY_INTERNAL_FACTORIES = "internal-factory-configurations";
+    
+    static final String KEY_INTERNAL_FWK_PROPERTIES = "internal-framework-properties";
+	
+	static final String KEY_TYPE = "type";
+	
+	static final String KEY_CARDINALITY = "cardinality";
+	
+    static final String KEY_VARIABLE = "variable";
+    
+    static final String KEY_RANGE = "range";
+
+    static final String KEY_MIN = "min";
+
+    static final String KEY_MAX = "max";
+
+    static final String KEY_INCLUDES = "includes";
+
+    static final String KEY_EXCLUDES = "excludes";
+
+    static final String KEY_OPTIONS = "options";
+  
+    static final String KEY_REGEX = "regex";
+
+    static final String KEY_VALUE = "value";
+
+    static final String KEY_REQUIRED = "required";
+
+    static final String KEY_OPERATIONS = "operations";
+
+    static final String KEY_INTERNAL_NAMES = "internal-names";
+
+    static final String KEY_REGION = "region";
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Operation.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Operation.java
new file mode 100644
index 0000000..312c2b2
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Operation.java
@@ -0,0 +1,26 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+/**
+ * Operations for factory configurations
+ */
+public enum Operation {
+    
+    CREATE, // allowed to create a factory configuration
+    UPDATE  // allowed to update a factory configuration
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Option.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Option.java
new file mode 100644
index 0000000..41d2c23
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Option.java
@@ -0,0 +1,85 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import java.io.IOException;
+
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+
+/**
+ * Option for a property value
+ */
+public class Option extends DescribableEntity {
+
+    /** The value for the option */
+    private String value;
+
+    /**
+     * Clear the object and reset to defaults
+     */
+    public void clear() {
+        super.clear();
+        this.setValue(null);
+    }
+
+    /**
+     * Extract the metadata from the JSON object.
+     * This method first calls {@link #clear()}
+     * @param jsonObj The JSON Object
+     * @throws IOException If JSON parsing fails
+     */
+    public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+        super.fromJSONObject(jsonObj);
+        try {
+          	this.setValue(this.getString(InternalConstants.KEY_VALUE));
+        } catch (final JsonException | IllegalArgumentException e) {
+            throw new IOException(e);
+        }
+    }
+    
+    /**
+     * Get the value for the option
+  	 * @return the value or {@code null}
+     */
+    public String getValue() {
+        return value;
+    }
+
+    /**
+     * Set the value for the option
+     * @param value the value to set
+     */
+    public void setValue(final String value) {
+        this.value = value;
+    }
+
+    /**
+     * Convert this object into JSON
+     *
+     * @return The json object builder
+     * @throws IOException If generating the JSON fails
+     */
+    JsonObjectBuilder createJson() throws IOException {
+        final JsonObjectBuilder objectBuilder = super.createJson();
+
+        this.setString(objectBuilder, InternalConstants.KEY_VALUE, this.getValue());
+
+        return objectBuilder;
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescription.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescription.java
new file mode 100644
index 0000000..4323937
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescription.java
@@ -0,0 +1,350 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import javax.json.Json;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonValue;
+
+/**
+ * Instances of this class represent a single configuration property
+ */
+public class PropertyDescription extends DescribableEntity {
+	
+	/** The property type */
+	private PropertyType type;
+
+	/** The property cardinality */
+    private int cardinality;
+
+	/** The optional variable */
+    private String variable;
+
+	/** The optional range */
+	private Range range;
+
+	/** The required includes for an array/collection (optional) */
+	private String[] includes;
+
+	/** The required excludes for an array/collection (optional) */
+	private String[] excludes;
+
+	/** The optional list of options for the value */
+	private List<Option> options;
+	
+	/** The optional regex */
+	private Pattern pattern;
+
+	/** Required? */
+	private boolean required;
+    
+    /**
+     * Create a new description
+     */
+    public PropertyDescription() {
+        this.setDefaults();
+    }
+
+    void setDefaults() {
+		this.setType(PropertyType.STRING);
+        this.setCardinality(1);
+        this.setRequired(false);
+    }
+
+    /**
+     * Clear the object and reset to defaults
+     */
+	public void clear() {
+        super.clear();
+        this.setDefaults();
+		this.setVariable(null);
+		this.setRange(null);
+		this.setIncludes(null);
+		this.setExcludes(null);
+		this.setOptions(null);
+		this.setRegex(null);
+    }
+
+	/**
+	 * Extract the metadata from the JSON object.
+	 * This method first calls {@link #clear()}
+	 * @param jsonObj The JSON Object
+	 * @throws IOException If JSON parsing fails
+	 */
+	public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+        super.fromJSONObject(jsonObj);
+        try {
+			this.setVariable(this.getString(InternalConstants.KEY_VARIABLE));
+			this.setCardinality(this.getInteger(InternalConstants.KEY_CARDINALITY, this.getCardinality()));
+			this.setRequired(this.getBoolean(InternalConstants.KEY_REQUIRED, this.isRequired()));
+			
+			final String typeVal = this.getString(InternalConstants.KEY_TYPE);
+			if ( typeVal != null ) {
+                this.setType(PropertyType.valueOf(typeVal.toUpperCase()));				
+			}
+			final JsonValue rangeVal = this.getAttributes().remove(InternalConstants.KEY_RANGE);
+			if ( rangeVal != null ) {
+				final Range range = new Range();
+				range.fromJSONObject(rangeVal.asJsonObject());
+				this.setRange(range);
+			}
+			final JsonValue incs = this.getAttributes().remove(InternalConstants.KEY_INCLUDES);
+			if ( incs != null ) {
+				final List<String> list = new ArrayList<>();
+				for(final JsonValue innerVal : incs.asJsonArray()) {
+                    list.add(getString(innerVal));
+                }
+                this.setIncludes(list.toArray(new String[list.size()]));
+			}
+			final JsonValue excs = this.getAttributes().remove(InternalConstants.KEY_EXCLUDES);
+			if ( excs != null ) {
+				final List<String> list = new ArrayList<>();
+				for(final JsonValue innerVal : excs.asJsonArray()) {
+                    list.add(getString(innerVal));
+                }
+                this.setExcludes(list.toArray(new String[list.size()]));
+			}
+			final JsonValue opts = this.getAttributes().remove(InternalConstants.KEY_OPTIONS);
+			if ( opts != null ) {
+				final List<Option> list = new ArrayList<>();
+				for(final JsonValue innerVal : opts.asJsonArray()) {
+					final Option o = new Option();
+					o.fromJSONObject(innerVal.asJsonObject());
+					list.add(o);
+                }
+				this.setOptions(list);
+			}
+			this.setRegex(this.getString(InternalConstants.KEY_REGEX));
+ 		} catch (final JsonException | IllegalArgumentException e) {
+            throw new IOException(e);
+        }
+	}
+	
+    /**
+     * Convert this object into JSON
+     *
+     * @return The json object builder
+     * @throws IOException If generating the JSON fails
+     */
+    JsonObjectBuilder createJson() throws IOException {
+		final JsonObjectBuilder objectBuilder = super.createJson();
+
+		if ( this.getType() != null && this.getType() != PropertyType.STRING ) {
+			this.setString(objectBuilder, InternalConstants.KEY_TYPE, this.getType().name());
+	    }
+		if ( this.getCardinality() != 1 ) {
+			objectBuilder.add(InternalConstants.KEY_CARDINALITY, this.getCardinality());
+		}
+		if ( this.isRequired() ) {
+			objectBuilder.add(InternalConstants.KEY_REQUIRED, this.isRequired());
+		}
+	    this.setString(objectBuilder, InternalConstants.KEY_VARIABLE, this.getVariable());
+		
+		if ( this.range != null ) {
+			objectBuilder.add(InternalConstants.KEY_RANGE, this.range.toJSONObject());
+		}
+		if ( this.includes != null && this.includes.length > 0 ) {
+			final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+			for(final String v : this.includes) {
+				arrayBuilder.add(v);
+			}
+			objectBuilder.add(InternalConstants.KEY_INCLUDES, arrayBuilder);
+		}
+		if ( this.excludes != null && this.excludes.length > 0 ) {
+			final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+			for(final String v : this.excludes) {
+				arrayBuilder.add(v);
+			}
+			objectBuilder.add(InternalConstants.KEY_EXCLUDES, arrayBuilder);
+		}
+		if ( this.options != null && !this.options.isEmpty()) {
+			final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+            for(final Option o : this.options) {
+				arrayBuilder.add(o.toJSONObject());
+			}
+			objectBuilder.add(InternalConstants.KEY_OPTIONS, arrayBuilder);
+		}
+		this.setString(objectBuilder, InternalConstants.KEY_REGEX, this.getRegex());
+		
+		return objectBuilder;
+	}
+
+    /**
+	 * Get the property type
+	 * @return the type
+	 */
+	public PropertyType getType() {
+		return type;
+	}
+
+	/**
+	 * Set the property type
+	 * @param type the type to set
+	 */
+	public void setType(final PropertyType type) {
+		this.type = type == null ? PropertyType.STRING : type;
+	}
+
+	/**
+	 * Get the cardinality
+	 * @return the cardinality
+	 */
+	public int getCardinality() {
+		return cardinality;
+	}
+
+	/**
+	 * Set the cardinality
+	 * @param cardinality the cardinality to set
+	 */
+	public void setCardinality(final int cardinality) {
+		this.cardinality = cardinality;
+	}
+
+	/**
+	 * Get the variable
+	 * @return the variable or {@code null}
+	 */
+	public String getVariable() {
+		return variable;
+	}
+
+	/**
+	 * Set the variable
+	 * @param variable the variable to set
+	 */
+	public void setVariable(final String variable) {
+		this.variable = variable;
+	}
+
+	/**
+	 * Get the range
+	 * @return the range or {@code null}
+	 */
+	public Range getRange() {
+		return range;
+	}
+
+	/**
+	 * Set the range
+	 * @param range the range to set
+	 */
+	public void setRange(final Range range) {
+		this.range = range;
+	}
+
+	/**
+	 * Get the includes
+	 * @return the includes or {@code null}
+	 */
+	public String[] getIncludes() {
+		return includes;
+	}
+
+	/**
+	 * Set the includes
+	 * @param includes the includes to set
+	 */
+	public void setIncludes(final String[] includes) {
+		this.includes = includes;
+	}
+
+	/**
+	 * Get the excludes
+	 * @return the excludes or {@code null}
+	 */
+	public String[] getExcludes() {
+		return excludes;
+	}
+
+	/**
+	 * Set the excludes
+	 * @param excludes the excludes to set
+	 */
+	public void setExcludes(final String[] excludes) {
+		this.excludes = excludes;
+	}
+
+	/**
+	 * Get the list of options
+	 * @return the options or {@code null}
+	 */
+	public List<Option> getOptions() {
+		return options;
+	}
+
+	/**
+	 * Set the list of options
+	 * @param options the options to set
+	 */
+	public void setOptions(final List<Option> options) {
+		this.options = options;
+	}
+
+	/**
+	 * Get the regex
+	 * @return the regex or {@code null}
+	 */
+	public String getRegex() {
+		return pattern == null ? null : pattern.pattern();
+	}
+
+	/**
+	 * Set the regex
+	 * @param regex the regex to set
+     * @throws IllegalArgumentException If the pattern is not valid
+	 */
+	public void setRegex(final String regex) {
+        if ( regex == null ) {
+            this.pattern = null;
+        } else {
+           this.pattern = Pattern.compile(regex);
+        }
+	}
+
+    /**
+     * Get the regex pattern
+     * @return The pattern or {@code null}
+     */
+    public Pattern getRegexPattern() {
+        return this.pattern;
+    }
+
+	/**
+	 * Is this property required?
+	 * @return {@code true} if it is required
+	 */
+	public boolean isRequired() {
+		return this.required;
+	}
+
+	/**
+	 * Set whether this property is required
+	 * @param flag The new value
+	 */
+	public void setRequired(final boolean flag) {
+		this.required = flag;
+	}
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyType.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyType.java
new file mode 100644
index 0000000..899f93a
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyType.java
@@ -0,0 +1,38 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+/**
+ * Property type
+ */
+public enum PropertyType {
+ 
+    STRING,
+    LONG,
+    INTEGER,
+    SHORT,
+    CHARACTER,
+    BYTE,
+    DOUBLE,
+    FLOAT,
+    BOOLEAN,
+    PASSWORD,
+    URL,
+    EMAIL,
+    PATH;
+ 
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Range.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Range.java
new file mode 100644
index 0000000..f5daf55
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Range.java
@@ -0,0 +1,121 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import java.io.IOException;
+
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+
+import org.apache.felix.cm.json.Configurations;
+
+/**
+ * A numerical value range
+ */
+public class Range extends AttributeableEntity {
+
+    /** The optional min value */
+    private Number min;
+
+    /** The optional max value */
+    private Number max;
+
+    /**
+     * Clear the object and reset to defaults
+     */
+	public void clear() {
+        super.clear();
+        this.setMax(null);
+        this.setMin(null);
+    }
+
+	/**
+	 * Extract the metadata from the JSON object.
+	 * This method first calls {@link #clear()}
+	 * @param jsonObj The JSON Object
+	 * @throws IOException If JSON parsing fails
+	 */
+	public void fromJSONObject(final JsonObject jsonObj) throws IOException {
+        super.fromJSONObject(jsonObj);
+        try {
+			this.setMin(this.getNumber(InternalConstants.KEY_MIN));
+			this.setMax(this.getNumber(InternalConstants.KEY_MAX));
+ 		} catch (final JsonException | IllegalArgumentException e) {
+            throw new IOException(e);
+        }
+	}
+
+	/**
+     * Get the min value
+	 * @return the min or {@code null}
+	 */
+	public Number getMin() {
+		return min;
+	}
+
+	/**
+     * Set the min value
+	 * @param min the min to set
+	 */
+	public void setMin(final Number min) {
+		this.min = min;
+	}
+
+	/**
+     * Get the max value
+	 * @return the max or {@code null}
+	 */
+	public Number getMax() {
+		return max;
+	}
+
+	/**
+     * Set the max value
+	 * @param max the max to set
+	 */
+	public void setMax(final Number max) {
+		this.max = max;
+    }
+
+	/**
+     * Convert this object into JSON
+     *
+     * @return The json object builder
+     * @throws IOException If generating the JSON fails
+     */
+    JsonObjectBuilder createJson() throws IOException {
+		final JsonObjectBuilder objectBuilder = super.createJson();
+
+        if ( this.getMin() != null ) {
+            objectBuilder.add(InternalConstants.KEY_MIN, Configurations.convertToJsonValue(this.getMin()));
+        }
+        if ( this.getMax() != null ) {
+            objectBuilder.add(InternalConstants.KEY_MAX, Configurations.convertToJsonValue(this.getMax()));
+        }
+
+		return objectBuilder;
+	}
+
+	/* (non-Javadoc)
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString() {
+		return "Range [min=" + getMax() + ", max=" + getMax() + "]";
+	}	
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Region.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Region.java
new file mode 100644
index 0000000..d4c14f4
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Region.java
@@ -0,0 +1,24 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+public enum Region {
+    
+    GLOBAL,
+    INTERNAL;
+
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/package-info.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/package-info.java
new file mode 100644
index 0000000..b67f74d
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.sling.feature.extension.apiregions.api.config;
+
+
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidationResult.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidationResult.java
new file mode 100644
index 0000000..f1cb9f4
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidationResult.java
@@ -0,0 +1,60 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config.validation;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ConfigurationValidationResult {
+
+    private final Map<String, PropertyValidationResult> propertyResults = new HashMap<>();
+
+    private final List<String> globalErrors = new ArrayList<>();
+    
+    private final List<String> warnings = new ArrayList<>();
+
+    public boolean isValid() {
+        boolean valid = globalErrors.isEmpty();
+        if ( valid ) {
+            for(final PropertyValidationResult r : this.propertyResults.values()) {
+                if ( !r.isValid() ) {
+                    valid = false;
+                    break;
+                }
+            }
+        }
+        return valid;
+    }
+
+    public List<String> getGlobalErrors() {
+        return this.globalErrors;
+    }
+    
+    public Map<String, PropertyValidationResult> getPropertyResults() {
+        return propertyResults;
+    }
+
+    /**
+     * Return the list of warnings
+     * @return The list of warnings - might be empty
+     */
+    public List<String> getWarnings() {
+        return this.warnings;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidator.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidator.java
new file mode 100644
index 0000000..a7e505d
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidator.java
@@ -0,0 +1,104 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config.validation;
+
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurableEntity;
+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.PropertyDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.Region;
+import org.osgi.framework.Constants;
+
+/**
+ * Validator to validate a configuration
+ */
+public class ConfigurationValidator {
+    
+    /**
+     * List of properties which are always allowed
+     */
+    public static final List<String> ALLOWED_PROPERTIES = Arrays.asList(Constants.SERVICE_DESCRIPTION,
+        Constants.SERVICE_VENDOR,
+        Constants.SERVICE_RANKING);
+
+
+    private final PropertyValidator propertyValidator = new PropertyValidator();
+
+    /**
+     * Validate a configuration
+     * @param config The OSGi configuration
+     * @param desc The configuration description 
+     * @param region The optional region for the configuration
+     * @return The result
+     */
+    public ConfigurationValidationResult validate(final Configuration config, final ConfigurableEntity desc, final Region region) {
+        final ConfigurationValidationResult result = new ConfigurationValidationResult();
+        if ( config.isFactoryConfiguration() ) {
+            if ( !(desc instanceof FactoryConfigurationDescription) ) {
+                result.getGlobalErrors().add("Factory configuration cannot be validated against non factory configuration description");
+            } else {
+                validateProperties(desc, config, result.getPropertyResults(), region);
+            }
+        } else {
+            if ( !(desc instanceof ConfigurationDescription) ) {
+                result.getGlobalErrors().add("Configuration cannot be validated against factory configuration description");
+            } else {
+                validateProperties(desc, config, result.getPropertyResults(), region);
+            }
+        }
+
+        if ( desc.getDeprecated() != null ) {
+            result.getWarnings().add(desc.getDeprecated());
+        }
+        return result;
+    }
+
+    void validateProperties(final ConfigurableEntity desc, 
+            final Configuration configuration, 
+            final Map<String, PropertyValidationResult> results,
+            final Region region) {
+        final Dictionary<String, Object> properties = configuration.getConfigurationProperties();
+        for(final Map.Entry<String, PropertyDescription> propEntry : desc.getPropertyDescriptions().entrySet()) {
+            final Object value = properties.get(propEntry.getKey());
+            final PropertyValidationResult result = propertyValidator.validate(value, propEntry.getValue());
+            results.put(propEntry.getKey(), result);
+        }
+        final Enumeration<String> keyEnum = properties.keys();
+        while ( keyEnum.hasMoreElements() ) {
+            final String propName = keyEnum.nextElement();
+            if ( !desc.getPropertyDescriptions().containsKey(propName) ) {
+                final PropertyValidationResult result = new PropertyValidationResult();
+                results.put(propName, result);
+                if ( Constants.SERVICE_RANKING.equals(propName) ) {
+                    final Object value = properties.get(propName);
+                    if ( !(value instanceof Integer) ) {
+                        result.getErrors().add("service.ranking must be of type Integer");
+                    }    
+                } else if ( !ALLOWED_PROPERTIES.contains(propName) && region != Region.INTERNAL ) {
+                    result.getErrors().add("Property is not allowed");
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..d757845
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidationResult.java
@@ -0,0 +1,69 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config.validation;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Validation result for a feature
+ */
+public class FeatureValidationResult {
+
+    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
+     */
+    public boolean isValid() {
+        boolean valid = true;
+        for(final ConfigurationValidationResult r : this.configurationResults.values()) {
+            if ( !r.isValid() ) {
+                valid = false;
+                break;
+            }
+        }
+        if ( valid ) {
+            for(final PropertyValidationResult r : this.frameworkPropertyResults.values()) {
+                if ( !r.isValid() ) {
+                    valid = false;
+                    break;
+                }
+            }
+        }
+        return valid;
+    }
+
+    /**
+     * Get the confiugration validation results.
+     * @return The results keyed by configuration PIDs
+     */
+    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
new file mode 100644
index 0000000..63cdc37
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidator.java
@@ -0,0 +1,212 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config.validation;
+
+import java.util.List;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.builder.FeatureProvider;
+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;
+
+/**
+ * Validator to validate a feature
+ */
+public class FeatureValidator {
+    
+    private final ConfigurationValidator configurationValidator = new ConfigurationValidator();
+
+    private final PropertyValidator propertyValidator = new PropertyValidator();
+
+    private FeatureProvider featureProvider;
+
+    /**
+     * Get the current feature provider
+	 * @return the feature provider or {@code null}
+	 */
+	public FeatureProvider getFeatureProvider() {
+		return featureProvider;
+	}
+
+	/**
+     * Set the feature provider
+	 * @param featureProvider the feature provider to set
+	 */
+	public void setFeatureProvider(final FeatureProvider provider) {
+		this.featureProvider = provider;
+    }
+    
+    /**
+     * Validate the feature against the configuration API
+     * @param feature The feature
+     * @param api The configuration API
+     * @return A {@code FeatureValidationResult}
+     * @throws IllegalArgumentException If api is {@code null}
+     */
+    public FeatureValidationResult validate(final Feature feature, final ConfigurationApi api) {
+        final FeatureValidationResult result = new FeatureValidationResult();
+        if ( api == null ) {
+            throw new IllegalArgumentException();
+        }
+
+        for(final Configuration config : feature.getConfigurations()) {
+            final RegionInfo regionInfo = getRegionInfo(feature, config);
+
+            if ( regionInfo == null ) {
+                final ConfigurationValidationResult cvr = new ConfigurationValidationResult();
+                cvr.getGlobalErrors().add("Unable to properly validate configuration, region info cannot be determined");
+                result.getConfigurationResults().put(config.getPid(), cvr);
+            } else {
+                if ( config.isFactoryConfiguration() ) {
+                    final FactoryConfigurationDescription desc = api.getFactoryConfigurationDescriptions().get(config.getFactoryPid());
+                    if ( desc != null ) {
+                        final ConfigurationValidationResult r = configurationValidator.validate(config, desc, regionInfo.region);
+                        result.getConfigurationResults().put(config.getPid(), r);
+                        if ( regionInfo.region != Region.INTERNAL ) {
+                            if ( desc.getOperations().isEmpty() ) {
+                                r.getGlobalErrors().add("No operations allowed for factory configuration");
+                            } else {
+                                if ( regionInfo.isUpdate && !desc.getOperations().contains(Operation.UPDATE)) {
+                                    r.getGlobalErrors().add("Updating of factory configuration is not allowed");
+                                } else if ( !regionInfo.isUpdate && !desc.getOperations().contains(Operation.CREATE)) {
+                                    r.getGlobalErrors().add("Creation of factory configuration is not allowed");
+                                }
+                            }
+                            if ( desc.getInternalNames().contains(config.getName())) {
+                                r.getGlobalErrors().add("Factory configuration with name is not allowed");
+                            }
+                        }                        
+
+                    } else if ( regionInfo.region != Region.INTERNAL && api.getInternalFactoryConfigurations().contains(config.getFactoryPid())) {
+                        final ConfigurationValidationResult cvr = new ConfigurationValidationResult();
+                        cvr.getGlobalErrors().add("Factory configuration is not allowed");
+                        result.getConfigurationResults().put(config.getPid(), cvr);
+                    }
+                } else {
+                    final ConfigurationDescription desc = api.getConfigurationDescriptions().get(config.getPid());
+                    if ( desc != null ) {
+                        final ConfigurationValidationResult r = configurationValidator.validate(config, desc, regionInfo.region);
+                        result.getConfigurationResults().put(config.getPid(), r);
+                    } else if ( regionInfo.region!= Region.INTERNAL && api.getInternalConfigurations().contains(config.getPid())) {
+                        final ConfigurationValidationResult cvr = new ConfigurationValidationResult();
+                        cvr.getGlobalErrors().add("Configuration is not allowed");
+                        result.getConfigurationResults().put(config.getPid(), cvr);
+                    } 
+                }    
+            }
+
+            // 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;
+    }
+
+    static final class RegionInfo {
+        
+        public Region region;
+
+        public boolean isUpdate;
+    }
+
+    RegionInfo getRegionInfo(final Feature feature, final Configuration cfg) {
+        final FeatureProvider provider = this.getFeatureProvider();
+        final RegionInfo result = new RegionInfo();
+        
+        final List<ArtifactId> list = cfg.getFeatureOrigins();
+        if ( !list.isEmpty() ) {
+            boolean global = false;
+            for(final ArtifactId id : list) {
+                final Feature f = provider == null ? null : provider.provide(id);
+                if ( f == null ) {
+                    return null;
+                }
+                final ConfigurationApi api = ConfigurationApi.getConfigurationApi(f);
+                if ( api == null || api.getRegion() != Region.INTERNAL ) {
+                    global = true;
+                    break;
+                }
+            }
+            result.region = global ? Region.GLOBAL : Region.INTERNAL;
+            result.isUpdate = list.size() > 1;
+        } else {
+            final ConfigurationApi api = ConfigurationApi.getConfigurationApi(feature);
+            if ( api == null || api.getRegion() == null || api.getRegion() == Region.GLOBAL ) {
+                result.region = Region.GLOBAL;
+            } else {
+                result.region = Region.INTERNAL;
+            }
+            result.isUpdate = false;
+        }
+        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/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidationResult.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidationResult.java
new file mode 100644
index 0000000..b68ef2e
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidationResult.java
@@ -0,0 +1,52 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config.validation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PropertyValidationResult {
+
+    private final List<String> errors = new ArrayList<>();
+
+    private final List<String> warnings = new ArrayList<>();
+
+    /**
+     * Is the property value valid?
+     * @return {@code true} if the value is valid
+     */
+	public boolean isValid() {
+        return errors.isEmpty();
+    }
+
+    /**
+     * If {@link #isValid()} returns {@code false} this returns
+     * a list of human readable errors.
+     * @return A list of errors - empty if {@link #isValid()} returns {@code true}
+     */
+	public List<String> getErrors() {
+        return errors;
+    }
+
+    /**
+     * Return the list of warnings
+     * @return The list of warnings - might be empty
+     */
+    public List<String> getWarnings() {
+        return this.warnings;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidator.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidator.java
new file mode 100644
index 0000000..82f7a1c
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidator.java
@@ -0,0 +1,381 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config.validation;
+
+import java.lang.reflect.Array;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.sling.feature.extension.apiregions.api.config.Option;
+import org.apache.sling.feature.extension.apiregions.api.config.PropertyDescription;
+
+/**
+ * Validate a configuration property
+ */
+public class PropertyValidator {
+    
+	/**
+	 * Validate the value against the property definition
+	 * @return A property validation result
+	 */
+	public PropertyValidationResult validate(final Object value, final PropertyDescription prop) {
+		final PropertyValidationResult result = new PropertyValidationResult();
+		if ( value == null ) {
+            if ( prop.isRequired() ) {
+                result.getErrors().add("No value provided");
+            }
+		} else {
+			final List<Object> values;
+			if ( value.getClass().isArray() ) {
+				// array
+				values = new ArrayList<>();
+                for(int i=0;i<Array.getLength(value);i++) {
+					values.add(Array.get(value, i));
+				}
+			} else if ( value instanceof Collection ) { 
+				// collection
+				values = new ArrayList<>();
+				final Collection<?> c = (Collection<?>)value;
+				for(final Object o : c) {
+					values.add(o);
+				}
+			} else {
+				// single value
+				values = null;
+				validateValue(prop, value, result.getErrors());
+			}
+
+			if ( values != null ) {
+                // array or collection
+                for(final Object val : values) {
+                    validateValue(prop, val, result.getErrors());
+                }
+                validateList(prop, values, result.getErrors());
+            }
+            
+            if ( prop.getDeprecated() != null ) {
+                result.getWarnings().add(prop.getDeprecated());
+            }
+		}
+		return result;
+	}
+
+    /**
+     * Validate a multi value
+     * @param prop The property description
+     * @param values The values
+     * @param messages The messages to record errors
+     */
+    void validateList(final PropertyDescription prop, final List<Object> values, final List<String> messages) {
+        if ( prop.getCardinality() > 0 && values.size() > prop.getCardinality() ) {
+            messages.add("Array/collection contains too many elements, only " + prop.getCardinality() + " allowed");
+        }
+        if ( prop.getIncludes() != null ) {
+            for(final String inc : prop.getIncludes()) {
+                boolean found = false;
+                for(final Object val : values) {
+                    if ( inc.equals(val.toString())) {
+                        found = true;
+                        break;
+                    }
+                }
+                if ( !found ) {
+                    messages.add("Required included value " + inc + " not found");
+                }
+            }
+        }
+        if ( prop.getExcludes() != null ) {
+            for(final String exc : prop.getExcludes()) {
+                boolean found = false;
+                for(final Object val : values) {
+                    if ( exc.equals(val.toString())) {
+                        found = true;
+                        break;
+                    }
+                }
+                if ( found ) {
+                    messages.add("Required excluded value " + exc + " found");
+                }
+            }
+        }
+    }
+
+	void validateValue(final PropertyDescription prop, final Object value, final List<String> messages) {
+		if ( value != null ) {
+			switch ( prop.getType() ) {
+				case BOOLEAN : validateBoolean(prop, value, messages);
+							   break;
+				case BYTE : validateByte(prop, value, messages);
+							break;
+				case CHARACTER : validateCharacter(prop, value, messages);
+							break;
+				case DOUBLE : validateDouble(prop, value, messages); 
+							break;
+				case FLOAT : validateFloat(prop, value, messages); 
+							break;
+				case INTEGER : validateInteger(prop, value, messages);
+							break;
+				case LONG : validateLong(prop, value, messages);
+							break;
+				case SHORT : validateShort(prop, value, messages);
+							break;
+				case STRING : // no special validation for string
+							break;
+				case EMAIL : validateEmail(prop, value, messages); 
+							break;
+				case PASSWORD : validatePassword(prop, value, messages);
+							break;
+                case URL : validateURL(prop, value, messages);
+                           break;
+                case PATH : validatePath(prop, value, messages);
+							break;
+				default : messages.add("Unable to validate value - unknown property type : " + prop.getType());
+            }
+            validateRegex(prop, value, messages);
+            validateOptions(prop, value, messages);
+		} else {
+			messages.add("Null value provided for validation");
+		}
+	}
+	
+	void validateBoolean(final PropertyDescription prop, final Object value, final List<String> messages) {
+        if ( ! (value instanceof Boolean) ) {
+			if ( value instanceof String ) {
+				final String v = (String)value;
+				if ( ! v.equalsIgnoreCase("true") && !v.equalsIgnoreCase("false") ) {
+					messages.add("Boolean value must either be true or false, but not " + value);
+				}
+			} else {
+				messages.add("Boolean value must either be of type Boolean or String : " + value);
+			}
+		}
+	}
+
+	void validateByte(final PropertyDescription prop, final Object value, final List<String> messages) {
+        if ( ! (value instanceof Byte) ) {
+			if ( value instanceof String ) {
+				final String v = (String)value;
+				try {
+					validateRange(prop, Byte.valueOf(v), messages);
+				} catch ( final NumberFormatException nfe ) {
+                    messages.add("Value is not a valid Byte : " + value);
+                }
+            } else if ( value instanceof Number ) {
+                validateRange(prop, ((Number)value).byteValue(), messages);            
+			} else {
+				messages.add("Byte value must either be of type Byte or String : " + value);
+			}
+		} else {
+			validateRange(prop, (Byte)value, messages);
+		}
+	}
+
+	void validateShort(final PropertyDescription prop, final Object value, final List<String> messages) {
+        if ( ! (value instanceof Short) ) {
+			if ( value instanceof String ) {
+				final String v = (String)value;
+				try {
+					validateRange(prop, Short.valueOf(v), messages);
+				} catch ( final NumberFormatException nfe ) {
+                    messages.add("Value is not a valid Short : " + value);
+				}
+            } else if ( value instanceof Number ) {
+                validateRange(prop, ((Number)value).shortValue(), messages);            
+			} else {
+				messages.add("Short value must either be of type Short or String : " + value);
+			}
+		} else {
+			validateRange(prop, (Short)value, messages);
+		}
+	}
+
+	void validateInteger(final PropertyDescription prop, final Object value, final List<String> messages) {
+        if ( ! (value instanceof Integer) ) {
+			if ( value instanceof String ) {
+				final String v = (String)value;
+				try {
+					validateRange(prop, Integer.valueOf(v), messages);
+				} catch ( final NumberFormatException nfe ) {
+                    messages.add("Value is not a valid Integer : " + value);
+				}
+            } else if ( value instanceof Number ) {
+                validateRange(prop, ((Number)value).intValue(), messages);            
+			} else {
+				messages.add("Integer value must either be of type Integer or String : " + value);
+			}
+		} else {
+			validateRange(prop, (Integer)value, messages);
+		}
+	}
+
+	void validateLong(final PropertyDescription prop, final Object value, final List<String> messages) {
+        if ( ! (value instanceof Long) ) {
+			if ( value instanceof String ) {
+				final String v = (String)value;
+				try {
+					validateRange(prop, Long.valueOf(v), messages);
+				} catch ( final NumberFormatException nfe ) {
+                    messages.add("Value is not a valid Long : " + value);
+				}
+            } else if ( value instanceof Number ) {
+                validateRange(prop, ((Number)value).longValue(), messages);            
+			} else {
+				messages.add("Long value must either be of type Long or String : " + value);
+			}
+		} else {
+			validateRange(prop, (Long)value, messages);
+		}
+	}
+
+	void validateFloat(final PropertyDescription prop, final Object value, final List<String> messages) {
+        if ( ! (value instanceof Float) ) {
+			if ( value instanceof String ) {
+				final String v = (String)value;
+				try {
+					validateRange(prop, Float.valueOf(v), messages);
+				} catch ( final NumberFormatException nfe ) {
+                    messages.add("Value is not a valid Float : " + value);
+				}
+            } else if ( value instanceof Number ) {
+                validateRange(prop, ((Number)value).floatValue(), messages);            
+			} else {
+				messages.add("Float value must either be of type Float or String : " + value);
+			}
+		} else {
+			validateRange(prop, (Float)value, messages);
+		}
+	}
+
+	void validateDouble(final PropertyDescription prop, final Object value, final List<String> messages) {
+        if ( ! (value instanceof Double) ) {
+			if ( value instanceof String ) {
+				final String v = (String)value;
+				try {
+					validateRange(prop, Double.valueOf(v), messages);
+				} catch ( final NumberFormatException nfe ) {
+                    messages.add("Value is not a valid Double : " + value);
+				}
+            } else if ( value instanceof Number ) {
+                validateRange(prop, ((Number)value).doubleValue(), messages);            
+			} else {
+				messages.add("Double value must either be of type Double or String : " + value);
+			}
+		} else {
+			validateRange(prop, (Double)value, messages);
+		}
+	}
+
+	void validateCharacter(final PropertyDescription prop, final Object value, final List<String> messages) {
+        if ( ! (value instanceof Character) ) {
+			if ( value instanceof String ) {
+				final String v = (String)value;
+				if ( v.length() > 1 ) {
+                    messages.add("Value is not a valid Character : " + value);
+				}
+			} else {
+				messages.add("Character value must either be of type Character or String : " + value);
+			}
+		}
+	}
+
+	void validateURL(final PropertyDescription prop, final Object value, final List<String> messages) {
+		final String val = value.toString();
+		try {
+			new URL(val);
+		} catch ( final MalformedURLException mue) {
+			messages.add("Value is not a valid URL : " + val);
+		}
+	}
+
+	void validateEmail(final PropertyDescription prop, final Object value, final List<String> messages) {
+		final String val = value.toString();
+		// poor man's validation (should probably use InternetAddress)
+		if ( !val.contains("@") ) {
+			messages.add("Not a valid email address " + val);
+		}
+	}
+
+	void validatePassword(final PropertyDescription prop, final Object value, final List<String> messages) {
+		if ( prop.getVariable() == null ) {
+			messages.add("Value for a password must use a variable");
+		}
+	}
+
+	void validatePath(final PropertyDescription prop, final Object value, final List<String> messages) {
+		final String val = value.toString();
+		// poor man's validation 
+		if ( !val.startsWith("/") ) {
+			messages.add("Not a valid path " + val);
+		}
+	}
+
+    void validateRange(final PropertyDescription prop, final Number value, final List<String> messages) {
+	    if ( prop.getRange() != null ) {
+            if ( prop.getRange().getMin() != null ) {
+                if ( value instanceof Float || value instanceof Double ) {
+                    final double min = prop.getRange().getMin().doubleValue();
+                    if ( value.doubleValue() < min ) {
+                            messages.add("Value " + value + " is too low; should not be lower than " + prop.getRange().getMin());
+                    }    
+                } else {
+                    final long min = prop.getRange().getMin().longValue();
+                    if ( value.longValue() < min ) {
+                            messages.add("Value " + value + " is too low; should not be lower than " + prop.getRange().getMin());
+                    }    
+                }
+            }
+            if ( prop.getRange().getMax() != null ) {
+                if ( value instanceof Float || value instanceof Double ) {
+                    final double max = prop.getRange().getMax().doubleValue();
+                    if ( value.doubleValue() > max ) {
+                        messages.add("Value " + value + " is too high; should not be higher than " + prop.getRange().getMax());
+                    }    
+                } else {
+                    final long max = prop.getRange().getMax().longValue();
+                    if ( value.longValue() > max ) {
+                        messages.add("Value " + value + " is too high; should not be higher than " + prop.getRange().getMax());
+                    }    
+                }
+            }
+		}
+	}
+
+    void validateRegex(final PropertyDescription prop, final Object value, final List<String> messages) {
+        if ( prop.getRegexPattern() != null ) {
+            if ( !prop.getRegexPattern().matcher(value.toString()).matches() ) {
+                messages.add("Value " + value + " does not match regex " + prop.getRegex());
+            }
+        }
+    }
+
+    void validateOptions(final PropertyDescription prop, final Object value, final List<String> messages) {
+        if ( prop.getOptions() != null ) {
+            boolean found = false;
+            for(final Option opt : prop.getOptions()) {
+                if ( opt.getValue().equals(value.toString() ) ) {
+                    found = true; 
+                }
+            }
+            if ( !found ) {
+                messages.add("Value " + value + " does not match provided options");
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/package-info.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/package-info.java
new file mode 100644
index 0000000..247f925
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/validation/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.sling.feature.extension.apiregions.api.config.validation;
+
+
diff --git a/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask b/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask
index 0744c72..c2f9f10 100644
--- a/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask
+++ b/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask
@@ -4,3 +4,4 @@
 org.apache.sling.feature.extension.apiregions.analyser.CheckApiRegionsOrder
 org.apache.sling.feature.extension.apiregions.analyser.CheckApiRegionsBundleExportsImports
 org.apache.sling.feature.extension.apiregions.analyser.CheckApiRegionsCrossFeatureDups
+org.apache.sling.feature.extension.apiregions.analyser.CheckConfigurationApi
\ No newline at end of file
diff --git a/src/main/resources/META-INF/services/org.apache.sling.feature.builder.MergeHandler b/src/main/resources/META-INF/services/org.apache.sling.feature.builder.MergeHandler
index ec6db93..caa2534 100644
--- a/src/main/resources/META-INF/services/org.apache.sling.feature.builder.MergeHandler
+++ b/src/main/resources/META-INF/services/org.apache.sling.feature.builder.MergeHandler
@@ -1 +1,2 @@
 org.apache.sling.feature.extension.apiregions.APIRegionMergeHandler
+org.apache.sling.feature.extension.apiregions.ConfigurationApiMergeHandler
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandlerTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandlerTest.java
new file mode 100644
index 0000000..7c95589
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandlerTest.java
@@ -0,0 +1,197 @@
+/*
+ * 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.sling.feature.extension.apiregions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.Prototype;
+import org.apache.sling.feature.builder.BuilderContext;
+import org.apache.sling.feature.builder.FeatureBuilder;
+import org.apache.sling.feature.extension.apiregions.api.config.ConfigurationApi;
+import org.apache.sling.feature.extension.apiregions.api.config.Region;
+import org.junit.Test;
+
+public class ConfigurationApiMergeHandlerTest {
+
+    @Test public void testPrototypeRegionMerge() {
+        final Feature prototype = new Feature(ArtifactId.parse("g:p:1"));
+        final ConfigurationApi prototypeApi = new ConfigurationApi();
+        ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+
+        // always return prototype
+        final BuilderContext context = new BuilderContext(id -> prototype);
+        context.addMergeExtensions(new ConfigurationApiMergeHandler());
+        
+        final Feature feature = new Feature(ArtifactId.parse("g:f:1"));
+        feature.setPrototype(new Prototype(prototype.getId()));
+        final ConfigurationApi featureApi = new ConfigurationApi();
+        ConfigurationApi.setConfigurationApi(feature, featureApi);
+
+        // no region
+        Feature result = FeatureBuilder.assemble(feature, context);
+        ConfigurationApi api = ConfigurationApi.getConfigurationApi(result);
+        assertNotNull(api);
+        assertNull(api.getRegion());
+
+        // prototype has region
+        prototypeApi.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+        result = FeatureBuilder.assemble(feature, context);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.INTERNAL, api.getRegion());
+
+        prototypeApi.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+        result = FeatureBuilder.assemble(feature, context);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.GLOBAL, api.getRegion());
+
+        // feature has region
+        prototypeApi.setRegion(null);
+        ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+        featureApi.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(feature, featureApi);
+        result = FeatureBuilder.assemble(feature, context);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.INTERNAL, api.getRegion());
+
+        featureApi.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(feature, featureApi);
+        result = FeatureBuilder.assemble(feature, context);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.GLOBAL, api.getRegion());
+
+        // both have region
+        prototypeApi.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+        featureApi.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(feature, featureApi);
+        result = FeatureBuilder.assemble(feature, context);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.INTERNAL, api.getRegion());
+
+        prototypeApi.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+        featureApi.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(feature, featureApi);
+        result = FeatureBuilder.assemble(feature, context);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.INTERNAL, api.getRegion());
+
+        prototypeApi.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+        featureApi.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(feature, featureApi);
+        result = FeatureBuilder.assemble(feature, context);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.GLOBAL, api.getRegion());
+
+        prototypeApi.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(prototype, prototypeApi);
+        featureApi.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(feature, featureApi);
+        result = FeatureBuilder.assemble(feature, context);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.GLOBAL, api.getRegion());
+    }
+ 
+    @Test public void testRegionMerge() {
+        // always return prototype
+        final BuilderContext context = new BuilderContext(id -> null);
+        context.addMergeExtensions(new ConfigurationApiMergeHandler());
+
+        final Feature featureA = new Feature(ArtifactId.parse("g:a:1"));
+        final ConfigurationApi apiA = new ConfigurationApi();
+        ConfigurationApi.setConfigurationApi(featureA, apiA);
+        
+        final Feature featureB = new Feature(ArtifactId.parse("g:b:1"));
+        final ConfigurationApi apiB = new ConfigurationApi();
+        ConfigurationApi.setConfigurationApi(featureB, apiB);
+
+        // no region
+        final ArtifactId id = ArtifactId.parse("g:m:1");
+        Feature result = FeatureBuilder.assemble(id, context, featureA, featureB);
+        ConfigurationApi api = ConfigurationApi.getConfigurationApi(result);
+        assertNotNull(api);
+        assertNull(api.getRegion());
+
+        // only A has region
+        apiA.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(featureA, apiA);
+        result = FeatureBuilder.assemble(id, context, featureA, featureB);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.GLOBAL, api.getRegion());
+
+        apiA.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(featureA, apiA);
+        result = FeatureBuilder.assemble(id, context, featureA, featureB);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.GLOBAL, api.getRegion());
+
+        // only B has region
+        apiA.setRegion(null);
+        ConfigurationApi.setConfigurationApi(featureA, apiA);
+        apiB.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(featureB, apiB);
+        result = FeatureBuilder.assemble(id, context, featureA, featureB);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.GLOBAL, api.getRegion());
+
+        apiB.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(featureB, apiB);
+        result = FeatureBuilder.assemble(id, context, featureA, featureB);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.GLOBAL, api.getRegion());
+
+        // both have region
+        apiA.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(featureA, apiA);
+        apiB.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(featureB, apiB);
+        result = FeatureBuilder.assemble(id, context, featureA, featureB);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.INTERNAL, api.getRegion());
+
+        apiA.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(featureA, apiA);
+        apiB.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(featureB, apiB);
+        result = FeatureBuilder.assemble(id, context, featureA, featureB);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.GLOBAL, api.getRegion());
+
+        apiA.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(featureA, apiA);
+        apiB.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(featureB, apiB);
+        result = FeatureBuilder.assemble(id, context, featureA, featureB);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.GLOBAL, api.getRegion());
+
+        apiA.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(featureA, apiA);
+        apiB.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(featureB, apiB);
+        result = FeatureBuilder.assemble(id, context, featureA, featureB);
+        api = ConfigurationApi.getConfigurationApi(result);
+        assertEquals(Region.GLOBAL, api.getRegion());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntityTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntityTest.java
new file mode 100644
index 0000000..165a5ed
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntityTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+
+import javax.json.Json;
+import javax.json.JsonValue;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class AttributeableEntityTest {
+
+    public static class AE extends AttributeableEntity {
+        // AttributeableEntity is abstract, therefore subclassing for testing
+    }
+
+    @Test public void testClear() {
+        final AE entity = new AE();
+        entity.getAttributes().put("a", Json.createValue(5));
+        entity.clear();
+        assertTrue(entity.getAttributes().isEmpty());
+    }
+
+    @Test public void testFromJSONObject() throws IOException {
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"a\" : 1, \"b\" : \"2\"}");
+
+        final AE entity = new AE();
+        entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+        assertEquals(2, entity.getAttributes().size());
+        assertEquals(Json.createValue(1), entity.getAttributes().get("a"));
+        assertEquals(Json.createValue("2"), entity.getAttributes().get("b"));
+    }
+
+    @Test public void testToJSONObject() throws IOException {
+        final AE entity = new AE();
+        entity.getAttributes().put("a", Json.createValue(1));
+        entity.getAttributes().put("b", Json.createValue("2"));
+        
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"a\" : 1, \"b\" : \"2\"}");
+
+        assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+    }
+
+    @Test public void testGetString() {
+        final AE entity = new AE();
+        assertNull(entity.getString("foo"));
+        entity.getAttributes().put("foo", Json.createValue("bar"));
+        assertEquals("bar", entity.getString("foo"));
+        assertTrue(entity.getAttributes().isEmpty());
+    }
+
+    @Test public void testGetBoolean() throws IOException {
+        final AE entity = new AE();
+        assertTrue(entity.getBoolean("foo", true));
+
+        entity.getAttributes().put("foo", JsonValue.FALSE);
+        assertEquals(false, entity.getBoolean("foo", true));
+        assertTrue(entity.getAttributes().isEmpty());
+
+
+        try {
+            entity.getAttributes().put("foo", Json.createValue(1.0));
+            entity.getBoolean("foo", false);
+            fail();
+        } catch ( final IOException expected) {
+            // this is expected
+        }
+    }
+
+    @Test public void testGetInteger() throws IOException {
+        final AE entity = new AE();
+        assertEquals(7, entity.getInteger("foo", 7));
+
+        entity.getAttributes().put("foo", Json.createValue(9));
+        assertEquals(9, entity.getInteger("foo", 7));
+        assertTrue(entity.getAttributes().isEmpty());
+
+        entity.getAttributes().put("foo", Json.createValue("9"));
+        assertEquals(9, entity.getInteger("foo", 7));
+        assertTrue(entity.getAttributes().isEmpty());
+    }
+
+    @Test public void testGetNumber() throws IOException {
+        final AE entity = new AE();
+        assertNull(entity.getNumber("foo"));
+
+        entity.getAttributes().put("foo", Json.createValue(5));
+        assertEquals(5L, entity.getNumber("foo"));
+        assertTrue(entity.getAttributes().isEmpty());
+
+
+        try {
+            entity.getAttributes().put("foo", Json.createValue("a"));
+            entity.getNumber("foo");
+            fail();
+        } catch ( final IOException expected) {
+            // this is expected
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntityTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntityTest.java
new file mode 100644
index 0000000..896afc0
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntityTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class ConfigurableEntityTest {
+
+    public static class CE extends ConfigurableEntity {
+        // ConfigurableEntity is abstract, therefore subclassing for testing
+    }
+
+    @Test public void testClear() {
+        final CE entity = new CE();
+        entity.getAttributes().put("a", Json.createValue(5));
+        entity.setDeprecated("d");
+        entity.setTitle("t");
+        entity.setDescription("x");
+        entity.getPropertyDescriptions().put("a", new PropertyDescription());
+        entity.clear();
+        assertTrue(entity.getAttributes().isEmpty());
+        assertNull(entity.getDeprecated());
+        assertNull(entity.getTitle());
+        assertNull(entity.getDescription());
+        assertTrue(entity.getPropertyDescriptions().isEmpty());
+    }
+
+    @Test public void testFromJSONObject() throws IOException {
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"properties\" : { \"a\" : {}, \"b\" : {}}}");
+
+        final CE entity = new CE();
+        entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+        assertEquals(2, entity.getPropertyDescriptions().size());
+        assertNotNull(entity.getPropertyDescriptions().get("a"));
+        assertNotNull(entity.getPropertyDescriptions().get("b"));
+    }
+
+    @Test public void testToJSONObject() throws IOException {
+        final CE entity = new CE();
+        entity.getPropertyDescriptions().put("a", new PropertyDescription());
+        entity.getPropertyDescriptions().put("b", new PropertyDescription());
+
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"properties\" : { \"a\" : {}, \"b\" : {}}}");
+
+        assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApiTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApiTest.java
new file mode 100644
index 0000000..a362501
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApiTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.junit.Test;
+
+public class ConfigurationApiTest {
+
+    @Test public void testNullFeature() {
+        assertNull(ConfigurationApi.getConfigurationApi((Feature)null));
+    }
+
+    @Test public void testNullExtension() {
+        assertNull(ConfigurationApi.getConfigurationApi((Extension)null));
+        final Feature f = new Feature(ArtifactId.parse("g:a:1.0"));
+        assertNull(ConfigurationApi.getConfigurationApi(f));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testWrongExtensionType() {
+        final Feature f = new Feature(ArtifactId.parse("g:a:1.0"));
+        final Extension e = new Extension(ExtensionType.TEXT, ConfigurationApi.EXTENSION_NAME, ExtensionState.OPTIONAL);
+        f.getExtensions().add(e);
+        ConfigurationApi.getConfigurationApi(f);
+    }
+
+    @Test public void testSetConfigurationApi() {
+        final ConfigurationApi api = new ConfigurationApi();
+        final Feature f = new Feature(ArtifactId.parse("g:a:1"));
+
+        assertNull(f.getExtensions().getByName(ConfigurationApi.EXTENSION_NAME));
+
+        ConfigurationApi.setConfigurationApi(f, api);
+        assertNotNull(f.getExtensions().getByName(ConfigurationApi.EXTENSION_NAME));
+        assertNotNull(ConfigurationApi.getConfigurationApi(f));
+
+        ConfigurationApi.setConfigurationApi(f, null);
+        assertNull(f.getExtensions().getByName(ConfigurationApi.EXTENSION_NAME));
+    }
+
+    @Test public void testClear() {
+        final ConfigurationApi entity = new ConfigurationApi();
+        entity.getAttributes().put("a", Json.createValue(5));
+        entity.getConfigurationDescriptions().put("pid", new ConfigurationDescription());
+        entity.getFactoryConfigurationDescriptions().put("factory", new FactoryConfigurationDescription());
+        entity.getFrameworkPropertyDescriptions().put("prop", new FrameworkPropertyDescription());
+        entity.getInternalConfigurations().add("ipid");
+        entity.getInternalFactoryConfigurations().add("ifactory");
+        entity.getInternalFrameworkProperties().add("iprop");
+        entity.setRegion(Region.GLOBAL);
+        entity.clear();
+        assertTrue(entity.getAttributes().isEmpty());
+        assertTrue(entity.getConfigurationDescriptions().isEmpty());
+        assertTrue(entity.getFactoryConfigurationDescriptions().isEmpty());
+        assertTrue(entity.getFrameworkPropertyDescriptions().isEmpty());
+        assertTrue(entity.getInternalConfigurations().isEmpty());
+        assertTrue(entity.getInternalFactoryConfigurations().isEmpty());
+        assertTrue(entity.getInternalFrameworkProperties().isEmpty());
+        assertNull(entity.getRegion());
+    }
+
+    @Test public void testFromJSONObject() throws IOException {
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"a\" : 5, \"configurations\" : { \"pid\": {}}, " +
+            "\"factory-configurations\" : { \"factory\" : {}}," +
+            "\"framework-properties\" : { \"prop\" : { \"type\" : \"STRING\"}}," +
+            "\"internal-configurations\" : [\"ipid\"],"+
+            "\"internal-factory-configurations\" : [\"ifactory\"],"+
+            "\"internal-framework-properties\" : [\"iprop\"],"+
+            "\"region\" : \"INTERNAL\"}");
+
+        final ConfigurationApi entity = new ConfigurationApi();
+        entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+        assertEquals(1, entity.getConfigurationDescriptions().size());
+        assertEquals(1, entity.getFactoryConfigurationDescriptions().size());
+        assertEquals(1, entity.getFrameworkPropertyDescriptions().size());
+        assertEquals(1, entity.getInternalConfigurations().size());
+        assertEquals(1, entity.getInternalFactoryConfigurations().size());
+        assertEquals(1, entity.getInternalFrameworkProperties().size());
+        assertTrue(entity.getConfigurationDescriptions().containsKey("pid"));
+        assertTrue(entity.getFactoryConfigurationDescriptions().containsKey("factory"));
+        assertTrue(entity.getFrameworkPropertyDescriptions().containsKey("prop"));
+        assertTrue(entity.getInternalConfigurations().contains("ipid"));
+        assertTrue(entity.getInternalFactoryConfigurations().contains("ifactory"));
+        assertTrue(entity.getInternalFrameworkProperties().contains("iprop"));
+        assertEquals(Region.INTERNAL, entity.getRegion());
+    }
+
+    @Test public void testToJSONObject() throws IOException {
+        final ConfigurationApi entity = new ConfigurationApi();
+        entity.getAttributes().put("a", Json.createValue(5));
+        entity.getConfigurationDescriptions().put("pid", new ConfigurationDescription());
+        entity.getFactoryConfigurationDescriptions().put("factory", new FactoryConfigurationDescription());
+        entity.getFrameworkPropertyDescriptions().put("prop", new FrameworkPropertyDescription());
+        entity.getInternalConfigurations().add("ipid");
+        entity.getInternalFactoryConfigurations().add("ifactory");
+        entity.getInternalFrameworkProperties().add("iprop");
+        entity.setRegion(Region.INTERNAL);
+
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"a\" : 5, \"configurations\" : { \"pid\": {}}, " +
+            "\"factory-configurations\" : { \"factory\" : {}}," +
+            "\"framework-properties\" : { \"prop\" : {}}," +
+            "\"internal-configurations\" : [\"ipid\"],"+
+            "\"internal-factory-configurations\" : [\"ifactory\"],"+
+            "\"internal-framework-properties\" : [\"iprop\"],"+
+            "\"region\" : \"INTERNAL\"}");
+
+        assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());        
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntityTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntityTest.java
new file mode 100644
index 0000000..f2ad633
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntityTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class DescribableEntityTest {
+
+    public static class DE extends DescribableEntity {
+        // DescribableEntity is abstract, therefore subclassing for testing
+    }
+
+    @Test public void testClear() {
+        final DE entity = new DE();
+        entity.getAttributes().put("a", Json.createValue(5));
+        entity.setDeprecated("d");
+        entity.setTitle("t");
+        entity.setDescription("x");
+        entity.clear();
+        assertTrue(entity.getAttributes().isEmpty());
+        assertNull(entity.getDeprecated());
+        assertNull(entity.getTitle());
+        assertNull(entity.getDescription());
+    }
+
+    @Test public void testFromJSONObject() throws IOException {
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"a\" : 1, \"b\" : \"2\", \"title\" : \"t\", \"description\" : \"desc\", \"deprecated\" : \"depr\"}");
+
+        final DE entity = new DE();
+        entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+        assertEquals(2, entity.getAttributes().size());
+        assertEquals(Json.createValue(1), entity.getAttributes().get("a"));
+        assertEquals(Json.createValue("2"), entity.getAttributes().get("b"));
+        assertEquals("t", entity.getTitle());
+        assertEquals("desc", entity.getDescription());
+        assertEquals("depr", entity.getDeprecated());
+    }
+
+    @Test public void testToJSONObject() throws IOException {
+        final DE entity = new DE();
+        entity.getAttributes().put("a", Json.createValue(1));
+        entity.getAttributes().put("b", Json.createValue("2"));
+        entity.setTitle("t");
+        entity.setDescription("desc");
+        entity.setDeprecated("depr");
+
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"a\" : 1, \"b\" : \"2\", \"title\" : \"t\", \"description\" : \"desc\", \"deprecated\" : \"depr\"}");
+
+        assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfigurationDescriptionTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfigurationDescriptionTest.java
new file mode 100644
index 0000000..d396a71
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfigurationDescriptionTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class FactoryConfigurationDescriptionTest {
+
+    @Test public void testClear() {
+        final FactoryConfigurationDescription entity = new FactoryConfigurationDescription();
+        entity.getAttributes().put("a", Json.createValue(5));
+        entity.setDeprecated("d");
+        entity.setTitle("t");
+        entity.setDescription("x");
+        entity.getPropertyDescriptions().put("a", new PropertyDescription());
+        entity.getOperations().add(Operation.CREATE);
+        entity.getInternalNames().add("internal");
+        entity.clear();
+        assertTrue(entity.getAttributes().isEmpty());
+        assertNull(entity.getDeprecated());
+        assertNull(entity.getTitle());
+        assertNull(entity.getDescription());
+        assertTrue(entity.getPropertyDescriptions().isEmpty());
+        assertEquals(2, entity.getOperations().size());
+        assertTrue(entity.getInternalNames().isEmpty());
+    }
+
+    @Test public void testFromJSONObject() throws IOException {
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"internal-names\" : [ \"a\", \"b\"], \"operations\" : [\"create\"]}");
+
+        final FactoryConfigurationDescription entity = new FactoryConfigurationDescription();
+        entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+        assertEquals(2, entity.getInternalNames().size());
+        assertTrue(entity.getInternalNames().contains("a"));
+        assertTrue(entity.getInternalNames().contains("b"));
+        assertEquals(1, entity.getOperations().size());
+        assertEquals(Operation.CREATE, entity.getOperations().iterator().next());
+    }
+
+    @Test public void testToJSONObject() throws IOException {
+        final FactoryConfigurationDescription entity = new FactoryConfigurationDescription();
+        entity.getInternalNames().add("a");
+        entity.getInternalNames().add("b");
+        entity.getOperations().clear();
+        entity.getOperations().add(Operation.UPDATE);
+
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"internal-names\" : [ \"a\", \"b\"], \"operations\" : [\"UPDATE\"]}");
+
+        assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/OptionTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/OptionTest.java
new file mode 100644
index 0000000..924c2b0
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/OptionTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class OptionTest {
+
+    @Test public void testClear() {
+        final Option entity = new Option();
+        entity.getAttributes().put("a", Json.createValue(5));
+        entity.setDeprecated("d");
+        entity.setTitle("t");
+        entity.setDescription("x");
+        entity.setValue("foo");
+        entity.clear();
+        assertTrue(entity.getAttributes().isEmpty());
+        assertNull(entity.getDeprecated());
+        assertNull(entity.getTitle());
+        assertNull(entity.getDescription());
+        assertNull(entity.getValue());
+    }
+
+    @Test public void testFromJSONObject() throws IOException {
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"a\" : 1, \"b\" : \"2\", \"title\" : \"t\", \"description\" : \"desc\", \"value\" : \"v\" }");
+
+        final Option entity = new Option();
+        entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+        assertEquals(2, entity.getAttributes().size());
+        assertEquals(Json.createValue(1), entity.getAttributes().get("a"));
+        assertEquals(Json.createValue("2"), entity.getAttributes().get("b"));
+        assertEquals("t", entity.getTitle());
+        assertEquals("desc", entity.getDescription());
+        assertEquals("v", entity.getValue());
+    }
+
+    @Test public void testToJSONObject() throws IOException {
+        final Option entity = new Option();
+        entity.getAttributes().put("a", Json.createValue(1));
+        entity.getAttributes().put("b", Json.createValue("2"));
+        entity.setTitle("t");
+        entity.setDescription("desc");
+        entity.setValue("v");
+
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"a\" : 1, \"b\" : \"2\", \"title\" : \"t\", \"description\" : \"desc\", \"value\" : \"v\" }");
+
+        assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescriptionTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescriptionTest.java
new file mode 100644
index 0000000..d9b50c2
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescriptionTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Arrays;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class PropertyDescriptionTest {
+
+    @Test public void testClear() {
+        final PropertyDescription entity = new PropertyDescription();
+        entity.getAttributes().put("a", Json.createValue(5));
+        entity.setDeprecated("d");
+        entity.setTitle("t");
+        entity.setDescription("x");
+        entity.setCardinality(5);
+        entity.setExcludes(new String[] {"ex"});
+        entity.setIncludes(new String[] {"in"});
+        entity.setOptions(Arrays.asList(new Option()));
+        entity.setRange(new Range());
+        entity.setRegex(".");
+        entity.setRequired(true);
+        entity.setVariable("var");
+        entity.setType(PropertyType.BYTE);        
+        entity.clear();
+        assertTrue(entity.getAttributes().isEmpty());
+        assertNull(entity.getDeprecated());
+        assertNull(entity.getTitle());
+        assertNull(entity.getDescription());
+        assertEquals(1, entity.getCardinality());
+        assertNull(entity.getExcludes());
+        assertNull(entity.getIncludes());
+        assertNull(entity.getOptions());
+        assertNull(entity.getRange());
+        assertNull(entity.getRegex());
+        assertNull(entity.getRegexPattern());
+        assertNull(entity.getVariable());
+        assertFalse(entity.isRequired());
+        assertEquals(PropertyType.STRING, entity.getType());
+    }
+
+    @Test public void testFromJSONObject() throws IOException {
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"type\" : \"BYTE\", \"cardinality\": 5, \"required\" : true, \"variable\" : \"var\"," +
+        "\"range\" : {}, \"includes\" : [\"in\"], \"excludes\" : [\"ex\"] , \"options\": [{}], \"regex\": \".\"}");
+
+        final PropertyDescription entity = new PropertyDescription();
+        entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+
+        assertEquals(5, entity.getCardinality());
+        assertEquals(PropertyType.BYTE, entity.getType());
+        assertTrue(entity.isRequired());
+        assertEquals("var", entity.getVariable());
+        assertNotNull(entity.getRange());
+        assertArrayEquals(new String[] {"ex"}, entity.getExcludes());
+        assertArrayEquals(new String[] {"in"}, entity.getIncludes());
+        assertEquals(1, entity.getOptions().size());
+        assertEquals(".", entity.getRegex());
+        assertNotNull(entity.getRegexPattern());
+
+        // test defaults and empty values
+        ext.setJSON("{ \"variable\" : \"var\", \"regex\": \".\"}");
+        entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+
+        assertEquals(1, entity.getCardinality());
+        assertEquals(PropertyType.STRING, entity.getType());
+        assertFalse(entity.isRequired());
+        assertEquals("var", entity.getVariable());
+        assertNull(entity.getRange());
+        assertNull(entity.getExcludes());
+        assertNull(entity.getIncludes());
+        assertNull(entity.getOptions());
+        assertEquals(".", entity.getRegex());
+        assertNotNull(entity.getRegexPattern());
+   }
+
+    @Test public void testToJSONObject() throws IOException {
+        final PropertyDescription entity = new PropertyDescription();
+        entity.setCardinality(5);
+        entity.setExcludes(new String[] {"ex"});
+        entity.setIncludes(new String[] {"in"});
+        entity.setOptions(Arrays.asList(new Option()));
+        entity.setRange(new Range());
+        entity.setRegex(".");
+        entity.setRequired(true);
+        entity.setVariable("var");
+        entity.setType(PropertyType.BYTE);
+
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"type\" : \"BYTE\", \"cardinality\": 5, \"required\" : true, \"variable\" : \"var\"," +
+            "\"range\" : {}, \"includes\" : [\"in\"], \"excludes\" : [\"ex\"] , \"options\": [{}], \"regex\": \".\"}");
+
+        assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+
+        // test defaults and empty values
+        entity.setCardinality(1);
+        entity.setType(null);
+        entity.setRequired(false);
+        entity.setRange(null);
+        entity.setOptions(null);
+        entity.setExcludes(null);
+        entity.setIncludes(null);
+
+        ext.setJSON("{ \"variable\" : \"var\", \"regex\": \".\"}");
+
+        assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/RangeTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/RangeTest.java
new file mode 100644
index 0000000..b894daf
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/RangeTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import javax.json.Json;
+
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.junit.Test;
+
+public class RangeTest {
+
+    @Test public void testClear() {
+        final Range entity = new Range();
+        entity.getAttributes().put("a", Json.createValue(5));
+        entity.setMax(5);
+        entity.setMin(20.1);
+        entity.clear();
+        assertTrue(entity.getAttributes().isEmpty());
+        assertNull(entity.getMax());
+        assertNull(entity.getMin());
+    }
+
+    @Test public void testFromJSONObject() throws IOException {
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"min\" : 5, \"max\" : 20.1 }");
+
+        final Range entity = new Range();
+        entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
+        assertEquals(5L, entity.getMin());
+        assertEquals(20.1, entity.getMax());
+    }
+
+    @Test public void testToJSONObject() throws IOException {
+        final Range entity = new Range();
+        entity.setMin(5);
+        entity.setMax(20.1);
+
+        final Extension ext = new Extension(ExtensionType.JSON, "a", ExtensionState.OPTIONAL);
+        ext.setJSON("{ \"min\" : 5, \"max\" : 20.1 }");
+
+        assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidatorTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidatorTest.java
new file mode 100644
index 0000000..46c1f57
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/ConfigurationValidatorTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config.validation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.sling.feature.Configuration;
+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.PropertyDescription;
+import org.apache.sling.feature.extension.apiregions.api.config.PropertyType;
+import org.apache.sling.feature.extension.apiregions.api.config.Region;
+import org.junit.Test;
+import org.osgi.framework.Constants;
+
+public class ConfigurationValidatorTest {
+
+    private final ConfigurationValidator validator = new ConfigurationValidator();
+
+    @Test public void testWrongDescriptionTypeForConfiguration() {
+        final Configuration cfg = new Configuration("org.apache");
+        final FactoryConfigurationDescription fcd = new FactoryConfigurationDescription();
+
+        final ConfigurationValidationResult result = validator.validate(cfg, fcd, null);
+        assertFalse(result.isValid());
+        assertEquals(1, result.getGlobalErrors().size());
+    }
+
+    @Test public void testWrongDescriptionTypeForFactoryConfiguration() {
+        final Configuration cfg = new Configuration("org.apache~foo");
+        final ConfigurationDescription fcd = new ConfigurationDescription();
+
+        final ConfigurationValidationResult result = validator.validate(cfg, fcd, null);
+        assertFalse(result.isValid());
+        assertEquals(1, result.getGlobalErrors().size());
+    }
+
+    @Test public void testDeprecated() {
+        final Configuration cfg = new Configuration("org.apache");
+        final ConfigurationDescription cd = new ConfigurationDescription();
+        
+        ConfigurationValidationResult result = validator.validate(cfg, cd, null);
+        assertTrue(result.isValid());
+        assertTrue(result.getWarnings().isEmpty());
+
+        cd.setDeprecated("this is deprecated");
+        result = validator.validate(cfg, cd, null);
+        assertTrue(result.isValid());
+        assertFalse(result.getWarnings().isEmpty());
+        assertEquals("this is deprecated", result.getWarnings().get(0));
+    }
+
+    @Test public void testServiceRanking() {
+        final Configuration cfg = new Configuration("org.apache");
+        final ConfigurationDescription cd = new ConfigurationDescription();
+        cfg.getProperties().put(Constants.SERVICE_RANKING, 5); 
+
+        ConfigurationValidationResult result = validator.validate(cfg, cd, null);
+        assertTrue(result.isValid());
+
+        cfg.getProperties().put(Constants.SERVICE_RANKING, "5");
+        result = validator.validate(cfg, cd, null);
+        assertFalse(result.isValid());
+    }
+
+    @Test public void testAllowedProperties() {
+        final Configuration cfg = new Configuration("org.apache");
+        final ConfigurationDescription cd = new ConfigurationDescription();
+        cfg.getProperties().put(Constants.SERVICE_DESCRIPTION, "desc");
+        cfg.getProperties().put(Constants.SERVICE_VENDOR, "vendor");
+
+        ConfigurationValidationResult result = validator.validate(cfg, cd, null);
+        assertTrue(result.isValid());
+    }
+
+    @Test public void testAdditionalProperties() {
+        final Configuration cfg = new Configuration("org.apache");
+        cfg.getProperties().put("a", "desc");
+
+        final ConfigurationDescription cd = new ConfigurationDescription();
+        final PropertyDescription prop = new PropertyDescription();
+        cd.getPropertyDescriptions().put("a", prop);
+
+        ConfigurationValidationResult result = validator.validate(cfg, cd, Region.GLOBAL);
+        assertTrue(result.isValid());
+        assertEquals(1, result.getPropertyResults().size());
+        assertTrue(result.getPropertyResults().get("a").isValid());
+
+        cfg.getProperties().put("b", "vendor");
+        result = validator.validate(cfg, cd, Region.GLOBAL);
+        assertFalse(result.isValid());
+        assertEquals(2, result.getPropertyResults().size());
+        assertTrue(result.getPropertyResults().get("a").isValid());
+        assertFalse(result.getPropertyResults().get("b").isValid());
+
+        // allowed if internal
+        result = validator.validate(cfg, cd, Region.INTERNAL);
+        assertTrue(result.isValid());
+        assertEquals(2, result.getPropertyResults().size());
+    }
+
+    @Test public void testInvalidProperty() {
+        final Configuration cfg = new Configuration("org.apache");
+        cfg.getProperties().put("a", "desc");
+        cfg.getProperties().put("b", "vendor");
+
+        final ConfigurationDescription cd = new ConfigurationDescription();
+        final PropertyDescription propA = new PropertyDescription();
+        cd.getPropertyDescriptions().put("a", propA);
+        final PropertyDescription propB = new PropertyDescription();
+        propB.setType(PropertyType.INTEGER);
+        cd.getPropertyDescriptions().put("b", propB);
+
+        ConfigurationValidationResult result = validator.validate(cfg, cd, null);
+        assertFalse(result.isValid());
+        assertEquals(2, result.getPropertyResults().size());
+        assertTrue(result.getPropertyResults().get("a").isValid());
+        assertFalse(result.getPropertyResults().get("b").isValid());
+     }
+}
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
new file mode 100644
index 0000000..8d4130a
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/FeatureValidatorTest.java
@@ -0,0 +1,635 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config.validation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.builder.FeatureProvider;
+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;
+
+public class FeatureValidatorTest {
+    
+    private static final String PID = "org.apache.sling";
+
+    private static final String FACTORY_PID = "org.apache.sling.factory";
+
+    private final FeatureValidator validator = new FeatureValidator();
+
+    @Before public void setup() {
+        this.validator.setFeatureProvider(null);
+    }
+
+    private Feature createFeature(final String id) {
+        final Feature f= new Feature(ArtifactId.parse(id));
+        final Configuration c = new Configuration(PID);
+        c.getProperties().put("prop", "a");
+        f.getConfigurations().add(c);
+
+        final Configuration fc = new Configuration(FACTORY_PID.concat("~print"));
+        fc.getProperties().put("key", "value");
+        f.getConfigurations().add(fc);
+
+        f.getFrameworkProperties().put("prop", "1");
+
+        return f;
+    }
+
+    private ConfigurationApi createApi() {
+        final ConfigurationApi api = new ConfigurationApi();
+
+        final ConfigurationDescription cd = new ConfigurationDescription();
+        cd.getPropertyDescriptions().put("prop", new PropertyDescription());
+
+        api.getConfigurationDescriptions().put(PID, cd);
+
+        final FactoryConfigurationDescription fd = new FactoryConfigurationDescription();
+        fd.getPropertyDescriptions().put("key", new PropertyDescription());
+
+        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 testGetRegionInfoConfigurationNoOrigin() {
+        final Feature f1 = createFeature("g:a:1");
+        final Configuration cfg = f1.getConfigurations().getConfiguration(PID);
+
+        // no api set
+        FeatureValidator.RegionInfo info = validator.getRegionInfo(f1, cfg);
+        assertEquals(Region.GLOBAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // empty region in api
+        final ConfigurationApi api = createApi();
+        ConfigurationApi.setConfigurationApi(f1, api);
+        info = validator.getRegionInfo(f1, cfg);
+        assertEquals(Region.GLOBAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // global region in api
+        api.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        info = validator.getRegionInfo(f1, cfg);
+        assertEquals(Region.GLOBAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // internal region in api
+        api.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        info = validator.getRegionInfo(f1, cfg);
+        assertEquals(Region.INTERNAL, info.region);
+        assertFalse(info.isUpdate);
+    }
+     
+    @Test public void testGetRegionInfoConfigurationSingleOrigin() {
+        final Feature f1 = createFeature("g:a:1");
+        final Configuration cfg = f1.getConfigurations().getConfiguration(PID);
+
+        final Feature f2 = createFeature("g:b:1");
+        cfg.setFeatureOrigins(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, cfg);
+        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, cfg);
+        assertEquals(Region.GLOBAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // global in api
+        api2.setRegion(Region.GLOBAL);
+        ConfigurationApi.setConfigurationApi(f2, api2);
+        info = validator.getRegionInfo(f1, cfg);
+        assertEquals(Region.GLOBAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // internal in api
+        api2.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(f2, api2);
+        info = validator.getRegionInfo(f1, cfg);
+        assertEquals(Region.INTERNAL, info.region);
+        assertFalse(info.isUpdate);
+
+        // unknown id
+        this.validator.setFeatureProvider(id -> null);
+        cfg.setFeatureOrigins(Collections.singletonList(ArtifactId.parse("g:xy:1")));
+        info = validator.getRegionInfo(f1, cfg);
+        assertNull(info);
+    }
+
+    @Test public void testGetRegionInfoConfigurationMultipleOrigins() {
+        final Feature f1 = createFeature("g:a:1");
+        final Configuration cfg = f1.getConfigurations().getConfiguration(PID);
+
+        final Feature f2 = createFeature("g:b:1");
+        final Feature f3 = createFeature("g:c:1");
+        cfg.setFeatureOrigins(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, cfg);
+        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, cfg);
+        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, cfg);
+        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, cfg);
+        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, cfg);
+        assertEquals(Region.GLOBAL, info.region);
+        assertTrue(info.isUpdate);
+    }
+
+    @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);
+
+        FeatureValidationResult result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        // add property
+        f1.getConfigurations().getConfiguration(PID).getProperties().put("b", "x");
+        result = validator.validate(f1, api);
+        assertFalse(result.isValid());
+    }
+
+    @Test public void testInternalConfiguration() {
+        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 configurations as internal
+        api.getInternalConfigurations().add(PID);
+        api.getInternalFactoryConfigurations().add(FACTORY_PID);
+        ConfigurationApi.setConfigurationApi(f1, api);
+
+        // global region
+        result = validator.validate(f1, api);
+        assertFalse(result.isValid());
+        assertFalse(result.getConfigurationResults().get(PID).isValid());
+        assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~print")).isValid());
+
+        // internal region
+        api.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+    }
+
+    @Test public void testInternalFactoryNames() {
+        final Feature f1 = createFeature("g:a:1");
+
+        final Configuration fa = new Configuration(FACTORY_PID.concat("~a"));
+        fa.getProperties().put("key", "value");
+        f1.getConfigurations().add(fa);
+
+        final Configuration fb = new Configuration(FACTORY_PID.concat("~b"));
+        fb.getProperties().put("key", "value");
+        f1.getConfigurations().add(fb);
+
+        final ConfigurationApi api = createApi();
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getInternalNames().add("a");
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getInternalNames().add("b");
+        ConfigurationApi.setConfigurationApi(f1, api);
+
+        FeatureValidationResult result = validator.validate(f1, api);
+        assertFalse(result.isValid());
+        assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~a")).isValid());
+        assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~b")).isValid());
+        assertTrue(result.getConfigurationResults().get(FACTORY_PID.concat("~print")).isValid());
+
+       // internal region
+       api.setRegion(Region.INTERNAL);
+       ConfigurationApi.setConfigurationApi(f1, api);
+       result = validator.validate(f1, api);
+       assertTrue(result.isValid());
+    }
+
+    @Test public void testFactoryConfigurationOperationsWithCreate() {
+        final Feature f1 = createFeature("g:a:1");
+        final ConfigurationApi api = createApi();
+
+        // no operation -> fail
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+        ConfigurationApi.setConfigurationApi(f1, api);
+        FeatureValidationResult result = validator.validate(f1, api);
+        assertFalse(result.isValid());
+        assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~print")).isValid());
+
+        // only update -> fail
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.UPDATE);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertFalse(result.isValid());
+        assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~print")).isValid());
+
+        // only create -> success
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.CREATE);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        // update, create -> success
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.CREATE);
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.UPDATE);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        // internal region -> always success
+        api.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.UPDATE);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.CREATE);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+    }
+
+    @Test public void testFactoryConfigurationOperationsWithUpdate() {
+        final Feature f1 = createFeature("g:a:1");
+        final ConfigurationApi api = createApi();
+
+        final Configuration cfg = f1.getConfigurations().getConfiguration(FACTORY_PID.concat("~print"));
+
+        final Feature f2 = createFeature("g:b:1");
+        final Feature f3 = createFeature("g:c:1");
+        cfg.setFeatureOrigins(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 operation -> fail
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+        ConfigurationApi.setConfigurationApi(f1, api);
+        FeatureValidationResult result = validator.validate(f1, api);
+        assertFalse(result.isValid());
+        assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~print")).isValid());
+
+        // only update -> success
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.UPDATE);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        // only create -> fail
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.CREATE);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertFalse(result.isValid());
+        assertFalse(result.getConfigurationResults().get(FACTORY_PID.concat("~print")).isValid());
+
+        // update, create -> success
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.CREATE);
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.UPDATE);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        // internal region -> always success
+        api.setRegion(Region.INTERNAL);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        ConfigurationApi.setConfigurationApi(f2, api);
+        ConfigurationApi.setConfigurationApi(f3, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.UPDATE);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().add(Operation.CREATE);
+        ConfigurationApi.setConfigurationApi(f1, api);
+        result = validator.validate(f1, api);
+        assertTrue(result.isValid());
+
+        api.getFactoryConfigurationDescriptions().get(FACTORY_PID).getOperations().clear();
+        ConfigurationApi.setConfigurationApi(f1, api);
+        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());
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidatorTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidatorTest.java
new file mode 100644
index 0000000..acbffa5
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/validation/PropertyValidatorTest.java
@@ -0,0 +1,413 @@
+/*
+ * 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.sling.feature.extension.apiregions.api.config.validation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.sling.feature.extension.apiregions.api.config.Option;
+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.Range;
+import org.junit.Test;
+
+public class PropertyValidatorTest {
+    
+    private final PropertyValidator validator = new PropertyValidator();
+    
+    @Test public void testValidateWithNull() {
+        final PropertyDescription prop = new PropertyDescription();
+
+        // prop not required - no error
+        assertTrue(validator.validate(null, prop).getErrors().isEmpty());
+        assertTrue(validator.validate(null, prop).isValid());
+
+        // prop required - error
+        prop.setRequired(true);
+        assertEquals(1, validator.validate(null, prop).getErrors().size());
+        assertFalse(validator.validate(null, prop).isValid());
+    }
+
+    @Test public void testValidateBoolean() {
+        final PropertyDescription prop = new PropertyDescription();
+        prop.setType(PropertyType.BOOLEAN);
+
+        final List<String> messages = new ArrayList<>();
+
+        validator.validateBoolean(prop, Boolean.TRUE, messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateBoolean(prop, Boolean.FALSE, messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateBoolean(prop, "TRUE", messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateBoolean(prop, "FALSE", messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateBoolean(prop, "yes", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        validator.validateBoolean(prop, 1, messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+    }
+
+    @Test public void testValidateByte() {
+        final PropertyDescription prop = new PropertyDescription();
+        prop.setType(PropertyType.BYTE);
+
+        final List<String> messages = new ArrayList<>();
+
+        validator.validateByte(prop, (byte)1, messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateByte(prop, "1", messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateByte(prop, "yes", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        validator.validateByte(prop, 1, messages);
+        assertTrue(messages.isEmpty());
+    }
+
+    @Test public void testValidateShort() {
+        final PropertyDescription prop = new PropertyDescription();
+        prop.setType(PropertyType.SHORT);
+
+        final List<String> messages = new ArrayList<>();
+
+        validator.validateShort(prop, (short)1, messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateShort(prop, "1", messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateShort(prop, "yes", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        validator.validateShort(prop, 1, messages);
+        assertTrue(messages.isEmpty());
+    }
+
+    @Test public void testValidateInteger() {
+        final PropertyDescription prop = new PropertyDescription();
+        prop.setType(PropertyType.INTEGER);
+
+        final List<String> messages = new ArrayList<>();
+
+        validator.validateInteger(prop, 1, messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateInteger(prop, "1", messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateInteger(prop, "yes", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        validator.validateInteger(prop, 1, messages);
+        assertTrue(messages.isEmpty());
+    }
+
+    @Test public void testValidateLong() {
+        final PropertyDescription prop = new PropertyDescription();
+        prop.setType(PropertyType.LONG);
+
+        final List<String> messages = new ArrayList<>();
+
+        validator.validateLong(prop, 1L, messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateLong(prop, "1", messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateLong(prop, "yes", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        validator.validateLong(prop, 1, messages);
+        assertTrue(messages.isEmpty());
+    }
+
+    @Test public void testValidateFloat() {
+        final PropertyDescription prop = new PropertyDescription();
+        prop.setType(PropertyType.FLOAT);
+
+        final List<String> messages = new ArrayList<>();
+
+        validator.validateFloat(prop, 1.1, messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateFloat(prop, "1.1", messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateFloat(prop, "yes", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        validator.validateFloat(prop, 1, messages);
+        assertTrue(messages.isEmpty());
+    }
+
+    @Test public void testValidateDouble() {
+        final PropertyDescription prop = new PropertyDescription();
+        prop.setType(PropertyType.DOUBLE);
+
+        final List<String> messages = new ArrayList<>();
+
+        validator.validateDouble(prop, 1.1d, messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateDouble(prop, "1.1", messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateDouble(prop, "yes", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        validator.validateDouble(prop, 1, messages);
+        assertTrue(messages.isEmpty());
+    }
+
+    @Test public void testValidateChar() {
+        final PropertyDescription prop = new PropertyDescription();
+        prop.setType(PropertyType.CHARACTER);
+
+        final List<String> messages = new ArrayList<>();
+
+        validator.validateCharacter(prop, 'x', messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateCharacter(prop, "y", messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateCharacter(prop, "yes", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+    }
+
+    @Test public void testValidateUrl() {
+        final List<String> messages = new ArrayList<>();
+
+        validator.validateURL(null, "https://sling.apache.org/documentation", messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateURL(null, "hello world", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+    }
+
+    @Test public void testValidateEmail() {
+        final List<String> messages = new ArrayList<>();
+
+        validator.validateEmail(null, "a@b.com", messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateEmail(null, "hello world", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+    }
+
+    @Test public void testValidatePassword() {
+        final PropertyDescription prop = new PropertyDescription();
+        final List<String> messages = new ArrayList<>();
+
+        validator.validatePassword(prop, null, messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        prop.setVariable("secret");
+        validator.validatePassword(prop, null, messages);
+        assertTrue(messages.isEmpty());
+    }
+
+    @Test public void testValidatePath() {
+        final List<String> messages = new ArrayList<>();
+
+        validator.validatePath(null, "/a/b/c", messages);
+        assertTrue(messages.isEmpty());
+
+        validator.validateEmail(null, "hello world", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+    }
+    
+    @Test public void testValidateRange() {
+        final List<String> messages = new ArrayList<>();
+        final PropertyDescription prop = new PropertyDescription();
+
+        // no range set
+        validator.validateRange(prop, 2, messages);
+        assertTrue(messages.isEmpty());
+
+        // empty range set
+        prop.setRange(new Range());
+        validator.validateRange(prop, 2, messages);
+        assertTrue(messages.isEmpty());
+
+        // min set
+        prop.getRange().setMin(5);
+        validator.validateRange(prop, 5, messages);
+        assertTrue(messages.isEmpty());
+        validator.validateRange(prop, 6, messages);
+        assertTrue(messages.isEmpty());
+        validator.validateRange(prop, 4, messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        validator.validateRange(prop, 5.0, messages);
+        assertTrue(messages.isEmpty());
+        validator.validateRange(prop, 6.0, messages);
+        assertTrue(messages.isEmpty());
+        validator.validateRange(prop, 4.0, messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        // max set
+        prop.getRange().setMax(6);
+        validator.validateRange(prop, 5, messages);
+        assertTrue(messages.isEmpty());
+        validator.validateRange(prop, 6, messages);
+        assertTrue(messages.isEmpty());
+        validator.validateRange(prop, 7, messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        validator.validateRange(prop, 5.0, messages);
+        assertTrue(messages.isEmpty());
+        validator.validateRange(prop, 6.0, messages);
+        assertTrue(messages.isEmpty());
+        validator.validateRange(prop, 7.0, messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+    }   
+    
+    @Test public void testValidateRegex() {
+        final List<String> messages = new ArrayList<>();
+        final PropertyDescription prop = new PropertyDescription();
+
+        // no regex
+        validator.validateRegex(prop, "hello world", messages);
+        validator.validateRegex(prop, "world", messages);
+        assertTrue(messages.isEmpty());
+
+        // regex
+        prop.setRegex("h(.*)");
+        validator.validateRegex(prop, "hello world", messages);
+        assertTrue(messages.isEmpty());
+        validator.validateRegex(prop, "world", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+    }
+
+    @Test public void testValidateOptions() {
+        final List<String> messages = new ArrayList<>();
+        final PropertyDescription prop = new PropertyDescription();
+
+        // no options
+        validator.validateOptions(prop, "foo", messages);
+        validator.validateOptions(prop, "bar", messages);
+        assertTrue(messages.isEmpty());
+
+        // options
+        final List<Option> options = new ArrayList<>();
+        final Option o1 = new Option();
+        o1.setValue("foo");
+        final Option o2 = new Option();
+        o2.setValue("7");
+        options.add(o1);
+        options.add(o2);
+        prop.setOptions(options);
+        validator.validateOptions(prop, "foo", messages);
+        assertTrue(messages.isEmpty());
+        validator.validateOptions(prop, "bar", messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+        validator.validateOptions(prop, 7, messages);
+        assertTrue(messages.isEmpty());
+    }
+    
+    @Test public void testValidateList() {
+        final List<String> messages = new ArrayList<>();
+        final PropertyDescription prop = new PropertyDescription();
+
+        final List<Object> values = new ArrayList<>();
+        values.add("a");
+        values.add("b");
+        values.add("c");
+
+        // default cardinality - no excludes/includes
+        validator.validateList(prop, values, messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        // cardinality 3 - no excludes/includes
+        prop.setCardinality(3);
+        validator.validateList(prop, values, messages);
+        assertTrue(messages.isEmpty());
+
+        values.add("d");
+        validator.validateList(prop, values, messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        // excludes
+        prop.setExcludes(new String[] {"d", "e"});
+        validator.validateList(prop, values, messages);
+        assertEquals(2, messages.size()); // cardinality and exclude
+        messages.clear();
+
+        values.remove("d");
+        validator.validateList(prop, values, messages);
+        assertTrue(messages.isEmpty());
+
+        // includes
+        prop.setIncludes(new String[] {"b"});
+        validator.validateList(prop, values, messages);
+        assertTrue(messages.isEmpty());
+
+        prop.setIncludes(new String[] {"x"});
+        validator.validateList(prop, values, messages);
+        assertEquals(1, messages.size());
+        messages.clear();
+
+        values.add("x");
+        values.remove("a");
+        validator.validateList(prop, values, messages);
+        assertTrue(messages.isEmpty());
+    }
+
+    @Test public void testDeprecation() {
+        final PropertyDescription prop = new PropertyDescription();
+        prop.setDeprecated("This is deprecated");
+
+        final PropertyValidationResult result = validator.validate("foo", prop);
+        assertTrue(result.isValid());
+        assertEquals(1, result.getWarnings().size());
+        assertEquals("This is deprecated", result.getWarnings().get(0));
+    }
+}