Start implementing merge handler, add region support
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
index 7c83c71..b7d6b68 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandler.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandler.java
@@ -16,12 +16,18 @@
  */
 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
@@ -44,9 +50,51 @@
             // 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/api/config/ConfigurationApi.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/ConfigurationApi.java
index 33599d8..2d7af47 100644
--- 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
@@ -30,6 +30,7 @@
 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;
 
@@ -78,6 +79,32 @@
         }
     }
    
+    /**
+     * 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<>();
 
@@ -96,6 +123,9 @@
     /** 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
      */
@@ -107,6 +137,7 @@
         this.internalConfigurations.clear();
         this.internalFactories.clear();
         this.internalFrameworkProperties.clear();
+        this.setRegion(null);
     }
 
 	/**
@@ -119,6 +150,11 @@
     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 ) {
@@ -222,6 +258,22 @@
     }
 
     /**
+     * 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
@@ -229,6 +281,9 @@
      */
     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()) {
@@ -270,7 +325,7 @@
                 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/InternalConstants.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/config/InternalConstants.java
index 65922cd..2fff4a3 100644
--- 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
@@ -68,4 +68,6 @@
     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/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/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..5856910
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/ConfigurationApiMergeHandlerTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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());
+    }
+ }
\ 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
index f6beac6..a362501 100644
--- 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
@@ -17,6 +17,7 @@
 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;
 
@@ -51,6 +52,20 @@
         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));
@@ -60,6 +75,7 @@
         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());
@@ -68,6 +84,7 @@
         assertTrue(entity.getInternalConfigurations().isEmpty());
         assertTrue(entity.getInternalFactoryConfigurations().isEmpty());
         assertTrue(entity.getInternalFrameworkProperties().isEmpty());
+        assertNull(entity.getRegion());
     }
 
     @Test public void testFromJSONObject() throws IOException {
@@ -77,7 +94,8 @@
             "\"framework-properties\" : { \"prop\" : { \"type\" : \"STRING\"}}," +
             "\"internal-configurations\" : [\"ipid\"],"+
             "\"internal-factory-configurations\" : [\"ifactory\"],"+
-            "\"internal-framework-properties\" : [\"iprop\"]}");
+            "\"internal-framework-properties\" : [\"iprop\"],"+
+            "\"region\" : \"INTERNAL\"}");
 
         final ConfigurationApi entity = new ConfigurationApi();
         entity.fromJSONObject(ext.getJSONStructure().asJsonObject());
@@ -93,6 +111,7 @@
         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 {
@@ -104,6 +123,7 @@
         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\": {}}, " +
@@ -111,8 +131,9 @@
             "\"framework-properties\" : { \"prop\" : {}}," +
             "\"internal-configurations\" : [\"ipid\"],"+
             "\"internal-factory-configurations\" : [\"ifactory\"],"+
-            "\"internal-framework-properties\" : [\"iprop\"]}");
+            "\"internal-framework-properties\" : [\"iprop\"],"+
+            "\"region\" : \"INTERNAL\"}");
 
-        assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());
+        assertEquals(ext.getJSONStructure().asJsonObject(), entity.toJSONObject());        
     }
 }
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescriptionTest.java
similarity index 98%
rename from src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyTest.java
rename to src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescriptionTest.java
index 515d68d..d9b50c2 100644
--- a/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyTest.java
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/config/PropertyDescriptionTest.java
@@ -33,7 +33,7 @@
 import org.apache.sling.feature.ExtensionType;
 import org.junit.Test;
 
-public class PropertyTest {
+public class PropertyDescriptionTest {
 
     @Test public void testClear() {
         final PropertyDescription entity = new PropertyDescription();