This is an automated email from the ASF dual-hosted git repository.

cziegeler pushed a commit to branch SLING-9867
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-feature-extension-apiregions.git


The following commit(s) were added to refs/heads/SLING-9867 by this push:
     new 3ae2797  SLING-9867 : Add extension to provide metadata about 
configuration api
3ae2797 is described below

commit 3ae2797bde1d7f7de7884a39e490ffa9adfa73f0
Author: Carsten Ziegeler <[email protected]>
AuthorDate: Mon Nov 2 08:44:02 2020 +0100

    SLING-9867 : Add extension to provide metadata about configuration api
---
 .gitignore                                         |   1 +
 .../apiregions/api/config/AttributeableEntity.java | 149 +++++++++
 .../apiregions/api/config/ConfigurableEntity.java  |  94 ++++++
 .../apiregions/api/config/Configuration.java       |  21 ++
 .../apiregions/api/config/ConfigurationApi.java    | 276 +++++++++++++++++
 .../extension/apiregions/api/config/Constants.java |  65 ++++
 .../apiregions/api/config/DescribableEntity.java   | 126 ++++++++
 .../api/config/FactoryConfiguration.java           | 120 ++++++++
 .../apiregions/api/config/FrameworkProperty.java   |  24 ++
 .../extension/apiregions/api/config/Operation.java |  26 ++
 .../extension/apiregions/api/config/Option.java    |  82 +++++
 .../extension/apiregions/api/config/Property.java  | 301 ++++++++++++++++++
 .../apiregions/api/config/PropertyType.java        |  37 +++
 .../extension/apiregions/api/config/Range.java     | 112 +++++++
 .../extension/apiregions/api/config/Validator.java | 341 +++++++++++++++++++++
 15 files changed, 1775 insertions(+)

diff --git a/.gitignore b/.gitignore
index 5b783ed..38f5ca4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,4 +14,5 @@ maven-eclipse.xml
 .vlt
 .DS_Store
 jcr.log
+.vscode
 atlassian-ide-plugin.xml
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..3d421dd
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/AttributeableEntity.java
@@ -0,0 +1,149 @@
+/*
+ * 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;
+
+public abstract class AttributeableEntity {
+       
+       /** The additional attributes */
+       private final Map<String, JsonValue> attributes = new LinkedHashMap<>();
+       
+       /**
+        * Create a new instance and set defaults.
+        */
+       public AttributeableEntity() {
+               this.clear();
+       }
+
+    /**
+     * Clear the object and remove all metadata
+     */
+       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 The attributes
+        */
+       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;
+       }
+
+       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
+        */
+       int getInteger(final String attributeName, final int defaultValue) {
+               final String val = this.getString(attributeName);
+               if ( val != null ) {
+                       return Integer.parseInt(val);
+               }
+               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..936ea1c
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurableEntity.java
@@ -0,0 +1,94 @@
+/*
+ * 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, Property> properties = new LinkedHashMap<>();
+
+    /**
+     * Clear the object and remove all metadata
+     */
+       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(Constants.KEY_PROPERTIES);
+            if ( val != null ) {
+                for(final Map.Entry<String, JsonValue> innerEntry : 
val.asJsonObject().entrySet()) {
+                                       final Property prop = new Property();
+                                       
prop.fromJSONObject(innerEntry.getValue().asJsonObject());
+                    this.getProperties().put(innerEntry.getKey(), prop);
+                }
+            }            
+        } catch (final JsonException | IllegalArgumentException e) {
+            throw new IOException(e);
+        }
+       }
+
+       /**
+        * Get the properties
+        * @return The properties
+        */
+    public Map<String, Property> getProperties() {
+        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.getProperties().isEmpty() ) {
+                       final JsonObjectBuilder propBuilder = 
Json.createObjectBuilder();
+                       for(final Map.Entry<String, Property> entry : 
this.getProperties().entrySet()) {
+                               propBuilder.add(entry.getKey(), 
entry.getValue().createJson());
+                       }
+                       objBuilder.add(Constants.KEY_PROPERTIES, propBuilder);
+               }
+
+               return objBuilder;
+   }
+}
diff --git 
a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Configuration.java
 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Configuration.java
new file mode 100644
index 0000000..5f95df2
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Configuration.java
@@ -0,0 +1,21 @@
+/*
+ * 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 class Configuration extends ConfigurableEntity {
+
+}
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..b6ce23f
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApi.java
@@ -0,0 +1,276 @@
+/*
+ * 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.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}.
+     * @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);
+        }
+    }
+   
+    /** The map of configurations */
+       private final Map<String, Configuration> configurations = new 
LinkedHashMap<>();
+
+    /** The map of factory configurations */
+    private final Map<String, FactoryConfiguration> factories = new 
LinkedHashMap<>();
+
+    /** The map of framework properties */
+    private final Map<String, FrameworkProperty> 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<>();
+    
+    /**
+     * Clear the object and remove all metadata
+     */
+    public void clear() {
+        super.clear();
+        this.configurations.clear();
+        this.factories.clear();
+        this.frameworkProperties.clear();
+        this.internalConfigurations.clear();
+        this.internalFactories.clear();
+        this.internalFrameworkProperties.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(Constants.KEY_CONFIGURATIONS);
+            if ( val != null ) {
+                for(final Map.Entry<String, JsonValue> innerEntry : 
val.asJsonObject().entrySet()) {
+                    final Configuration cfg = new Configuration();
+                    cfg.fromJSONObject(innerEntry.getValue().asJsonObject());
+                    this.getConfigurations().put(innerEntry.getKey(), cfg);
+                }
+            }
+            
+            val = this.getAttributes().remove(Constants.KEY_FACTORIES);
+            if ( val != null ) {
+                for(final Map.Entry<String, JsonValue> innerEntry : 
val.asJsonObject().entrySet()) {
+                    final FactoryConfiguration cfg = new 
FactoryConfiguration();
+                    cfg.fromJSONObject(innerEntry.getValue().asJsonObject());
+                    this.getFactories().put(innerEntry.getKey(), cfg);
+                }
+            }
+
+            val = this.getAttributes().remove(Constants.KEY_FWK_PROPERTIES);
+            if ( val != null ) {
+                for(final Map.Entry<String, JsonValue> innerEntry : 
val.asJsonObject().entrySet()) {
+                    final FrameworkProperty cfg = new FrameworkProperty();
+                    cfg.fromJSONObject(innerEntry.getValue().asJsonObject());
+                    this.getFrameworkProperties().put(innerEntry.getKey(), 
cfg);
+                }
+            }
+
+            val = 
this.getAttributes().remove(Constants.KEY_INTERNAL_CONFIGURATIONS);
+            if ( val != null ) {
+                for(final JsonValue innerVal : val.asJsonArray()) {
+                    this.getInternalConfigurations().add(getString(innerVal));
+                }
+            }
+
+            val = 
this.getAttributes().remove(Constants.KEY_INTERNAL_FACTORIES);
+            if ( val != null ) {
+                for(final JsonValue innerVal : val.asJsonArray()) {
+                    this.getInternalFactories().add(getString(innerVal));
+                }
+            }
+
+            val = 
this.getAttributes().remove(Constants.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 configurations
+        * @return the configurations
+        */
+       public Map<String, Configuration> getConfigurations() {
+               return configurations;
+       }
+
+       /**
+     * Get the factory configurations
+        * @return the factories
+        */
+       public Map<String, FactoryConfiguration> getFactories() {
+               return factories;
+       }
+
+       /**
+     * Get the framework properties
+        * @return the frameworkProperties
+        */
+       public Map<String, FrameworkProperty> getFrameworkProperties() {
+               return frameworkProperties;
+       }
+
+       /**
+     * Get the internal configuration names
+        * @return the internalConfigurations
+        */
+       public List<String> getInternalConfigurations() {
+               return internalConfigurations;
+       }
+
+       /**
+     * Get the internal factory names
+        * @return the internalFactories
+        */
+       public List<String> getInternalFactories() {
+               return internalFactories;
+       }
+
+       /**
+     * Get the internal framework property names
+        * @return the internalFrameworkProperties
+        */
+       public List<String> getInternalFrameworkProperties() {
+               return internalFrameworkProperties;
+    }
+
+    /**
+     * 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.getConfigurations().isEmpty() ) {
+            final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+            for(final Map.Entry<String, Configuration> entry : 
this.getConfigurations().entrySet()) {
+                propBuilder.add(entry.getKey(), entry.getValue().createJson());
+            }
+            objBuilder.add(Constants.KEY_CONFIGURATIONS, propBuilder);
+        }
+        if ( !this.getFactories().isEmpty() ) {
+            final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+            for(final Map.Entry<String, FactoryConfiguration> entry : 
this.getFactories().entrySet()) {
+                propBuilder.add(entry.getKey(), entry.getValue().createJson());
+            }
+            objBuilder.add(Constants.KEY_FACTORIES, propBuilder);
+        }
+        if ( !this.getFrameworkProperties().isEmpty() ) {
+            final JsonObjectBuilder propBuilder = Json.createObjectBuilder();
+            for(final Map.Entry<String, FrameworkProperty> entry : 
this.getFrameworkProperties().entrySet()) {
+                propBuilder.add(entry.getKey(), entry.getValue().createJson());
+            }
+            objBuilder.add(Constants.KEY_FWK_PROPERTIES, propBuilder);
+        }
+        if ( !this.getInternalConfigurations().isEmpty() ) {
+            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+            for(final String n : this.getInternalConfigurations()) {
+                arrayBuilder.add(n);
+            }
+                       objBuilder.add(Constants.KEY_INTERNAL_CONFIGURATIONS, 
arrayBuilder);
+               }
+               if ( !this.getInternalFactories().isEmpty() ) {
+            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+            for(final String n : this.getInternalFactories()) {
+                arrayBuilder.add(n);
+            }
+                       objBuilder.add(Constants.KEY_INTERNAL_FACTORIES, 
arrayBuilder);
+               }
+               if ( !this.getInternalFrameworkProperties().isEmpty() ) {
+            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+            for(final String n : this.getInternalFrameworkProperties()) {
+                arrayBuilder.add(n);
+            }
+                       objBuilder.add(Constants.KEY_INTERNAL_FWK_PROPERTIES, 
arrayBuilder);
+               }
+
+               return objBuilder;
+    }
+}
diff --git 
a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Constants.java
 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Constants.java
new file mode 100644
index 0000000..88f5788
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Constants.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;
+
+/**
+ * Constants used in this implementation
+ */
+public abstract class Constants {
+    
+    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 = "factories";
+    
+    static final String KEY_FWK_PROPERTIES = "framework-properties";
+
+    static final String KEY_INTERNAL_CONFIGURATIONS = 
"internal-configurations";
+
+    static final String KEY_INTERNAL_FACTORIES = "internal-factories";
+    
+    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";
+}
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..69c9e3a
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/DescribableEntity.java
@@ -0,0 +1,126 @@
+/*
+ * 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;
+
+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 remove all metadata
+     */
+       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(Constants.KEY_TITLE));
+                       
this.setDescription(this.getString(Constants.KEY_DESCRIPTION));
+                       
this.setDeprecated(this.getString(Constants.KEY_DEPRECATED));
+        } catch (final JsonException | IllegalArgumentException e) {
+            throw new IOException(e);
+               }
+       }
+
+       /**
+        * Get the title
+        * @return the title
+        */
+       public String getTitle() {
+               return title;
+       }
+
+       /**
+        * Set the title
+        * @param title the title to set
+        */
+       public void setTitle(String title) {
+               this.title = title;
+       }
+
+       /**
+        * Get the description
+        * @return the description
+        */
+       public String getDescription() {
+               return description;
+       }
+
+       /**
+        * Set the description
+        * @param description the description to set
+        */
+       public void setDescription(String description) {
+               this.description = description;
+       }
+
+       /**
+        * Get the deprecation text
+        * @return the deprecated
+        */
+       public String getDeprecated() {
+               return deprecated;
+       }
+
+       /**
+        * Set the deprecation text
+        * @param deprecated the deprecated to set
+        */
+       public void setDeprecated(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, Constants.KEY_TITLE, 
this.getTitle());
+               this.setString(objectBuilder, Constants.KEY_DESCRIPTION, 
this.getDescription());
+               this.setString(objectBuilder, Constants.KEY_DEPRECATED, 
this.getDeprecated());
+
+               return objectBuilder;
+       }
+}
diff --git 
a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfiguration.java
 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfiguration.java
new file mode 100644
index 0000000..c2345bb
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FactoryConfiguration.java
@@ -0,0 +1,120 @@
+/*
+ * 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;
+
+public class FactoryConfiguration extends ConfigurableEntity {
+    
+    private static final String KEY_OPERATIONS = "operations";
+
+    private static final String KEY_INTERNAL_NAMES = "internal-names";
+    
+    private final Set<Operation> operations = new HashSet<>();
+
+    private final List<String> internalNames = new ArrayList<>();
+
+    /**
+     * Clear the object and remove all metadata
+     */
+    public void clear() {
+        super.clear();
+               this.operations.clear();
+               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(KEY_OPERATIONS);
+            if ( val != null ) {
+                for(final JsonValue innerVal : val.asJsonArray()) {
+                    final String v = getString(innerVal).toUpperCase();
+                    this.getOperations().add(Operation.valueOf(v));
+                }
+            }
+            
+            val = this.getAttributes().remove(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);
+        }
+    }
+
+   /**
+        * @return the operations
+        */
+       public Set<Operation> getOperations() {
+               return operations;
+       }
+
+       /**
+        * @return the internalNames
+        */
+       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() ) {
+            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+            for(final Operation op : this.getOperations()) {
+                arrayBuilder.add(op.name());
+            }
+                       objBuilder.add(KEY_OPERATIONS, arrayBuilder);
+               }
+               if ( !this.getInternalNames().isEmpty() ) {
+            final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+            for(final String n : this.getInternalNames()) {
+                arrayBuilder.add(n);
+            }
+                       objBuilder.add(KEY_INTERNAL_NAMES, arrayBuilder);
+               }
+               return objBuilder;
+   }
+}
diff --git 
a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FrameworkProperty.java
 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FrameworkProperty.java
new file mode 100644
index 0000000..320448b
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/FrameworkProperty.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
+ */
+public class FrameworkProperty extends Property {
+    
+}
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..308143b
--- /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,
+    UPDATE
+}
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..12ba1e0
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Option.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+public class Option extends DescribableEntity {
+
+    /** The value for the option */
+    private String value;
+
+    /**
+     * Clear the object and remove all metadata
+     */
+       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(Constants.KEY_VALUE));
+               } catch (final JsonException | IllegalArgumentException e) {
+            throw new IOException(e);
+        }
+    }
+    
+    /**
+     * Get the value for the option
+        * @return the value
+        */
+       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, Constants.KEY_VALUE, 
this.getValue());
+
+               return objectBuilder;
+    }
+}
diff --git 
a/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Property.java
 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Property.java
new file mode 100644
index 0000000..b06ed54
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Property.java
@@ -0,0 +1,301 @@
+/*
+ * 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 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 Property 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 String regex;
+
+    /**
+     * Clear the object and remove all metadata
+     */
+       public void clear() {
+        super.clear();
+               this.setType(PropertyType.STRING);
+               this.setCardinality(1);
+               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(Constants.KEY_VARIABLE));
+                       
this.setCardinality(this.getInteger(Constants.KEY_CARDINALITY, 
this.getCardinality()));
+
+                       final String typeVal = 
this.getString(Constants.KEY_TYPE);
+                       if ( typeVal != null ) {
+                this.setType(PropertyType.valueOf(typeVal.toUpperCase()));     
                        
+                       }
+                       final JsonValue rangeVal = 
this.getAttributes().remove(Constants.KEY_RANGE);
+                       if ( rangeVal != null ) {
+                               final Range range = new Range();
+                               range.fromJSONObject(rangeVal.asJsonObject());
+                               this.setRange(range);
+                       }
+                       final JsonValue incs = 
this.getAttributes().remove(Constants.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(Constants.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(Constants.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(Constants.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.setString(objectBuilder, Constants.KEY_TYPE, 
this.getType().name());
+           }
+               if ( this.getCardinality() != 1 ) {
+                       objectBuilder.add(Constants.KEY_CARDINALITY, 
this.getCardinality());
+               }
+           this.setString(objectBuilder, Constants.KEY_VARIABLE, 
this.getVariable());
+               
+               if ( this.range != null ) {
+                       objectBuilder.add(Constants.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(Constants.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(Constants.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(Constants.KEY_OPTIONS, arrayBuilder);
+               }
+               this.setString(objectBuilder, Constants.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;
+       }
+
+       /**
+        * 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
+        */
+       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
+        */
+       public String getRegex() {
+               return regex;
+       }
+
+       /**
+        * Set the regex
+        * @param regex the regex to set
+        */
+       public void setRegex(final String regex) {
+               this.regex = regex;
+       }
+}
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..e0d2889
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyType.java
@@ -0,0 +1,37 @@
+/*
+ * 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,
+    INT,
+    SHORT,
+    CHAR,
+    BYTE,
+    DOUBLE,
+    FLOAT,
+    BOOLEAN,
+    PASSWORD,
+    URL,
+    EMAIL;
+ 
+}
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..01a474f
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Range.java
@@ -0,0 +1,112 @@
+/*
+ * 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;
+
+public class Range extends AttributeableEntity {
+
+    /** The optional min value */
+    private String min;
+
+    /** The optional max value */
+    private String max;
+
+    /**
+     * Clear the object and remove all metadata
+     */
+       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.getString(Constants.KEY_MIN));
+                       this.setMax(this.getString(Constants.KEY_MAX));
+               } catch (final JsonException | IllegalArgumentException e) {
+            throw new IOException(e);
+        }
+       }
+
+       /**
+     * Get the min value
+        * @return the min
+        */
+       public String getMin() {
+               return min;
+       }
+
+       /**
+     * Set the min value
+        * @param min the min to set
+        */
+       public void setMin(final String min) {
+               this.min = min;
+       }
+
+       /**
+     * Get the max value
+        * @return the max
+        */
+       public String getMax() {
+               return max;
+       }
+
+       /**
+     * Set the max value
+        * @param max the max to set
+        */
+       public void setMax(final String 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();
+
+               this.setString(objectBuilder, Constants.KEY_MIN, this.getMin());
+               this.setString(objectBuilder, Constants.KEY_MAX, 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/Validator.java
 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Validator.java
new file mode 100644
index 0000000..546fa1c
--- /dev/null
+++ 
b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/Validator.java
@@ -0,0 +1,341 @@
+/*
+ * 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.lang.reflect.Array;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Validate values
+ */
+public class Validator {
+       
+       public interface ValidationResult {
+
+               boolean isValid();
+
+               List<String> getErrors();
+       }
+
+       /**
+        * Validate the value against the property definition
+        * @return {@code null} if the value is valid, a human readable 
validation error message otherwise
+        */
+       public static ValidationResult validate(final Property prop, final 
Object value) {
+               final List<String> messages = new ArrayList<>();
+               if ( value == null ) {
+                       messages.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, messages);
+                       }
+
+                       if ( values != null ) {
+                               // array or collection
+                               if ( prop.getCardinality() > 0 && values.size() 
> prop.getCardinality() ) {
+                    messages.add("Array/collection contains too many elements, 
only " + prop.getCardinality() + " allowed");
+                               }
+                               for(final Object val : values) {
+                                       validateValue(prop, val, messages);
+                               }
+                               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");
+                                               }
+                                       }
+                               }
+                       }
+               }
+               return new ValidationResult(){
+                       
+                       public boolean isValid() {
+                               return messages.isEmpty();
+                       }
+
+                       public List<String> getErrors() {
+                               return messages;
+                       }       
+               };
+       }
+
+       static void validateValue(final Property 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 CHAR : validateChar(prop, value, messages);
+                                                       break;
+                               case DOUBLE : validateDouble(prop, value, 
messages); 
+                                                       break;
+                               case FLOAT : validateFloat(prop, value, 
messages); 
+                                                       break;
+                               case INT : validateInt(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;
+                               default : messages.add("Unable to validate 
value - unknown property type : " + prop.getType());
+                       }
+                       if ( prop.getRegex() != null ) {
+                               final Pattern p = 
Pattern.compile(prop.getRegex());
+                               if ( !p.matcher(value.toString()).matches() ) {
+                    messages.add("Value " + value + " does not match regex " + 
prop.getRegex());
+                               }
+                       }
+                       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");
+                               }
+                       }
+               } else {
+                       messages.add("Null value provided for validation");
+               }
+       }
+       
+       static void validateBoolean(final Property 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);
+                       }
+               }
+       }
+
+       static void validateByte(final Property 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 {
+                               messages.add("Byte value must either be of type 
Byte or String : " + value);
+                       }
+               } else {
+                       validateRange(prop, (Byte)value, messages);
+               }
+       }
+
+       static void validateShort(final Property 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 {
+                               messages.add("Short value must either be of 
type Short or String : " + value);
+                       }
+               } else {
+                       validateRange(prop, (Short)value, messages);
+               }
+       }
+
+       static void validateInt(final Property 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 {
+                               messages.add("Integer value must either be of 
type Integer or String : " + value);
+                       }
+               } else {
+                       validateRange(prop, (Integer)value, messages);
+               }
+       }
+
+       static void validateLong(final Property 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 {
+                               messages.add("Long value must either be of type 
Long or String : " + value);
+                       }
+               } else {
+                       validateRange(prop, (Long)value, messages);
+               }
+       }
+
+       static void validateFloat(final Property 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 {
+                               messages.add("Float value must either be of 
type Float or String : " + value);
+                       }
+               } else {
+                       validateRange(prop, (Float)value, messages);
+               }
+       }
+
+       static void validateDouble(final Property 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 {
+                               messages.add("Double value must either be of 
type Double or String : " + value);
+                       }
+               } else {
+                       validateRange(prop, (Double)value, messages);
+               }
+       }
+
+       static void validateChar(final Property 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);
+                       }
+               }
+       }
+
+       static void validateURL(final Property 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);
+               }
+       }
+
+       static void validateEmail(final Property 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);
+               }
+       }
+
+       static void validatePassword(final Property prop, final Object value, 
final List<String> messages) {
+               if ( prop.getVariable() == null ) {
+                       messages.add("Value for a password must use a 
variable");
+               }
+       }
+
+       static void validateRange(final Property prop, final Number value, 
final List<String> messages) {
+           if ( prop.getRange() != null ) {
+                       try {
+                if ( prop.getRange().getMin() != null ) {
+                                       final long min = 
Long.valueOf(prop.getRange().getMin());
+                                       if ( value.longValue() < min ) {
+                         messages.add("Value " + value + " is too low; should 
not be lower than " + prop.getRange().getMin());
+                                       }
+                               }
+                if ( prop.getRange().getMax() != null ) {
+                                       final long max = 
Long.valueOf(prop.getRange().getMax());
+                                       if ( value.longValue() > max ) {
+                         messages.add("Value " + value + " is too high; should 
not be higher than " + prop.getRange().getMax());
+                                       }
+                               }
+                       } catch ( final NumberFormatException nfe) {
+                               messages.add("Invalid range specified in " + 
prop.getRange());
+                       }
+               }
+       }
+
+}
\ No newline at end of file

Reply via email to