You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by cz...@apache.org on 2019/10/17 13:25:54 UTC

[sling-org-apache-sling-feature-extension-apiregions] 01/02: SLING-8783 : Create API for api regions

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

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

commit 864db681c4d301198461a349c57808deff2a5c73
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Thu Oct 17 11:15:10 2019 +0200

    SLING-8783 : Create API for api regions
---
 pom.xml                                            |  13 +-
 .../apiregions/APIRegionMergeHandler.java          | 170 +++----------
 .../extension/apiregions/AbstractHandler.java      |   7 +-
 .../apiregions/BundleArtifactFeatureHandler.java   |  15 +-
 .../extension/apiregions/BundleMappingHandler.java |  16 +-
 .../extension/apiregions/api/ApiExport.java        | 105 ++++++++
 .../extension/apiregions/api/ApiRegion.java        | 101 ++++++++
 .../extension/apiregions/api/ApiRegions.java       | 267 +++++++++++++++++++++
 .../extension/apiregions/api/package-info.java     |  23 ++
 .../apiregions/APIRegionMergeHandlerTest.java      |  87 ++++---
 .../extension/apiregions/api/TestApiRegions.java   |  65 +++++
 src/test/resources/json/apis.json                  |  15 ++
 12 files changed, 693 insertions(+), 191 deletions(-)

diff --git a/pom.xml b/pom.xml
index 33d1ecb..0c134eb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,7 +13,7 @@
     <parent>
         <groupId>org.apache.sling</groupId>
         <artifactId>sling</artifactId>
-        <version>34</version>
+        <version>35</version>
         <relativePath />
     </parent>
 
@@ -29,7 +29,7 @@
     </scm>
 
     <properties>
-        <jdk.version>7</jdk.version>
+        <jdk.version>8</jdk.version>
     </properties>
 
     <build>
@@ -48,6 +48,12 @@
     </build>
     <dependencies>
         <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.annotation.versioning</artifactId>
+            <version>1.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>org.apache.geronimo.specs</groupId>
             <artifactId>geronimo-json_1.0_spec</artifactId>
             <version>1.0-alpha-1</version>
@@ -56,12 +62,13 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.feature</artifactId>
-            <version>1.0.4</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>osgi.core</artifactId>
+            <version>6.0.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/APIRegionMergeHandler.java b/src/main/java/org/apache/sling/feature/extension/apiregions/APIRegionMergeHandler.java
index b055807..ec28400 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/APIRegionMergeHandler.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/APIRegionMergeHandler.java
@@ -16,159 +16,78 @@
  */
 package org.apache.sling.feature.extension.apiregions;
 
-import org.apache.sling.feature.Artifact;
-import org.apache.sling.feature.Extension;
-import org.apache.sling.feature.Feature;
-import org.apache.sling.feature.builder.HandlerContext;
-import org.apache.sling.feature.builder.MergeHandler;
-
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
-import java.io.StringReader;
-import java.io.StringWriter;
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Collection;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Properties;
-import java.util.Set;
 import java.util.stream.Collectors;
 
-import javax.json.Json;
 import javax.json.JsonArray;
-import javax.json.JsonObject;
-import javax.json.JsonReader;
-import javax.json.JsonString;
-import javax.json.JsonValue;
-import javax.json.stream.JsonGenerator;
 
-import static org.apache.sling.feature.extension.apiregions.AbstractHandler.API_REGIONS_NAME;
-import static org.apache.sling.feature.extension.apiregions.AbstractHandler.EXPORTS_KEY;
-import static org.apache.sling.feature.extension.apiregions.AbstractHandler.NAME_KEY;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.Extension;
+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.ApiExport;
+import org.apache.sling.feature.extension.apiregions.api.ApiRegion;
+import org.apache.sling.feature.extension.apiregions.api.ApiRegions;
 
+/**
+ * Merge to api region extensions
+ */
 public class APIRegionMergeHandler implements MergeHandler {
+
     @Override
     public boolean canMerge(Extension extension) {
-        return API_REGIONS_NAME.equals(extension.getName());
+        return ApiRegions.EXTENSION_NAME.equals(extension.getName());
     }
 
     @Override
     public void merge(HandlerContext context, Feature target, Feature source, Extension targetEx, Extension sourceEx) {
-        if (!sourceEx.getName().equals(API_REGIONS_NAME))
+        if (!sourceEx.getName().equals(ApiRegions.EXTENSION_NAME))
             return;
-        if (targetEx != null && !targetEx.getName().equals(API_REGIONS_NAME))
+        if (targetEx != null && !targetEx.getName().equals(ApiRegions.EXTENSION_NAME))
             return;
 
         storeBundleOrigins(context, source, target);
 
-        try (JsonReader srcJR = Json.createReader(new StringReader(sourceEx.getJSON()))) {
-            JsonArray srcJA = srcJR.readArray();
-
-            Map<String, Map<String, Object>> srcRegions = new LinkedHashMap<>();
-            for (int i=0; i < srcJA.size(); i++) {
-                String regionName = null;
-                Map<String, Object> region = new LinkedHashMap<>();
-                JsonObject jo = srcJA.getJsonObject(i);
-                for (Map.Entry<String, JsonValue> entry : jo.entrySet()) {
-                    Object val;
-                    switch (entry.getKey()) {
-                    case EXPORTS_KEY:
-                        val = readJsonArray((JsonArray) entry.getValue());
-                        break;
-                    default:
-                        val = ((JsonString) entry.getValue()).getString();
-                        if (NAME_KEY.equals(entry.getKey())) {
-                            regionName = val.toString();
-                        }
-                        break;
-                    }
-                    region.put(entry.getKey(), val);
-                }
-                if (regionName == null) {
-                    throw new IllegalStateException("No region name specified: " + sourceEx.getJSON());
-                }
-                srcRegions.put(regionName, region);
-            }
-
-            storeRegionOrigins(context, source, target, srcRegions.keySet());
+        final ApiRegions srcRegions = ApiRegions.parse((JsonArray) sourceEx.getJSONStructure());
 
-            JsonArray tgtJA;
-            if (targetEx != null) {
-                try (JsonReader tgtJR = Json.createReader(new StringReader(targetEx.getJSON()))) {
-                    tgtJA = tgtJR.readArray();
-                }
-            } else {
-                targetEx = new Extension(sourceEx.getType(), sourceEx.getName(), sourceEx.isRequired());
-                target.getExtensions().add(targetEx);
+        storeRegionOrigins(context, source, target, srcRegions);
 
-                tgtJA = Json.createArrayBuilder().build();
-            }
+        final ApiRegions targetRegions;
+        if (targetEx != null) {
+            targetRegions = ApiRegions.parse((JsonArray) targetEx.getJSONStructure());
+        } else {
+            targetEx = new Extension(sourceEx.getType(), sourceEx.getName(), sourceEx.getState());
+            target.getExtensions().add(targetEx);
 
-            StringWriter sw = new StringWriter();
-            JsonGenerator gen = Json.createGenerator(sw);
-            gen.writeStartArray();
-            for (JsonValue jv : tgtJA) {
-                if (jv instanceof JsonObject) {
-                    JsonObject jo = (JsonObject) jv;
-                    Map<String, Object> srcRegion = srcRegions.remove(jo.getString(NAME_KEY));
-                    if (srcRegion != null) {
-                        gen.writeStartObject();
-                        for (Map.Entry<String, JsonValue> entry : jo.entrySet()) {
-                            Object sp = srcRegion.get(entry.getKey());
-                            if (EXPORTS_KEY.equals(entry.getKey()) && sp instanceof List) {
-                                List<String> tgtPkgs = readJsonArray((JsonArray) entry.getValue());
-                                @SuppressWarnings("unchecked")
-                                List<String> srcPkgs = (List<String>) sp;
-                                for (String srcPkg : srcPkgs) {
-                                    if (!tgtPkgs.contains(srcPkg)) {
-                                        tgtPkgs.add(srcPkg);
-                                    }
-                                }
-                                gen.writeStartArray(entry.getKey());
-                                for (String p : tgtPkgs) {
-                                    gen.write(p);
-                                }
-                                gen.writeEnd();
-                            } else {
-                                gen.write(entry.getKey(), entry.getValue());
-                            }
-                        }
-                        gen.writeEnd();
-                    } else {
-                        gen.write(jv);
-                    }
-                }
-            }
+            targetRegions = new ApiRegions();
+        }
 
-            // If there are any remaining regions in the src extension, process them now
-            for (Map<String, Object> region : srcRegions.values()) {
-                gen.writeStartObject();
-                for (Map.Entry<String, Object> entry : region.entrySet()) {
-                    if (entry.getValue() instanceof Collection) {
-                        gen.writeStartArray(entry.getKey());
-                        for (Object o : (Collection<?>) entry.getValue()) {
-                            gen.write(o.toString());
-                        }
-                        gen.writeEnd();
-                    } else {
-                        gen.write(entry.getKey(), entry.getValue().toString());
+        for (final ApiRegion targetRegion : targetRegions.getRegions()) {
+            final ApiRegion sourceRegion = srcRegions.getRegionByName(targetRegion.getName());
+            if (sourceRegion != null) {
+                srcRegions.getRegions().remove(sourceRegion);
+                for (final ApiExport srcExp : sourceRegion.getExports()) {
+                    if (targetRegion.getExportByName(srcExp.getName()) == null) {
+                        targetRegion.getExports().add(srcExp);
                     }
                 }
-                gen.writeEnd();
             }
+        }
 
-            gen.writeEnd();
-            gen.close();
+        // If there are any remaining regions in the src extension, process them now
+        targetRegions.getRegions().addAll(srcRegions.getRegions());
 
-            targetEx.setJSON(sw.toString());
-        }
+        targetEx.setJSONStructure(targetRegions.toJSONArray());
     }
 
-    private void storeRegionOrigins(HandlerContext context, Feature source, Feature target, Set<String> regions) {
+    private void storeRegionOrigins(HandlerContext context, Feature source, Feature target, ApiRegions regions) {
         try {
             File f = AbstractHandler.getFeatureDataFile(context, target, "regionOrigins.properties");
 
@@ -180,7 +99,7 @@ public class APIRegionMergeHandler implements MergeHandler {
             }
 
             String fid = source.getId().toMvnId();
-            p.put(fid, regions.stream().collect(Collectors.joining(",")));
+            p.put(fid, regions.getRegions().stream().map(region -> region.getName()).collect(Collectors.joining(",")));
 
             try (FileOutputStream fos = new FileOutputStream(f)) {
                 p.store(fos, "Mapping from feature ID to regions that the feature is a member of");
@@ -225,17 +144,4 @@ public class APIRegionMergeHandler implements MergeHandler {
             throw new IllegalStateException("Problem storing bundle origin information", e);
         }
     }
-
-    private static List<String> readJsonArray(JsonArray jsonArray) {
-        List<String> l = new ArrayList<>();
-        for (JsonValue jv : jsonArray) {
-            if (jv instanceof JsonString) {
-                String pkg = ((JsonString) jv).getString();
-                if (!pkg.startsWith("#")) { // ignore comment lines
-                    l.add(pkg);
-                }
-            }
-        }
-        return l;
-    }
 }
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/AbstractHandler.java b/src/main/java/org/apache/sling/feature/extension/apiregions/AbstractHandler.java
index fd94692..78ddb25 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/AbstractHandler.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/AbstractHandler.java
@@ -16,9 +16,6 @@
  */
 package org.apache.sling.feature.extension.apiregions;
 
-import org.apache.sling.feature.Feature;
-import org.apache.sling.feature.builder.HandlerContext;
-
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
@@ -31,8 +28,10 @@ import java.nio.file.Path;
 import java.util.Date;
 import java.util.Properties;
 
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.builder.HandlerContext;
+
 class AbstractHandler {
-    static final String API_REGIONS_NAME = "api-regions";
     static final String GLOBAL_NAME = "global";
 
     static final String NAME_KEY = "name";
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/BundleArtifactFeatureHandler.java b/src/main/java/org/apache/sling/feature/extension/apiregions/BundleArtifactFeatureHandler.java
index 967e69e..a3df8ff 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/BundleArtifactFeatureHandler.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/BundleArtifactFeatureHandler.java
@@ -16,12 +16,6 @@
  */
 package org.apache.sling.feature.extension.apiregions;
 
-import org.apache.sling.feature.Artifact;
-import org.apache.sling.feature.Extension;
-import org.apache.sling.feature.Feature;
-import org.apache.sling.feature.builder.HandlerContext;
-import org.apache.sling.feature.builder.PostProcessHandler;
-
 import java.io.File;
 import java.io.IOException;
 import java.io.StringReader;
@@ -39,10 +33,17 @@ import javax.json.JsonObject;
 import javax.json.JsonReader;
 import javax.json.JsonValue;
 
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.builder.HandlerContext;
+import org.apache.sling.feature.builder.PostProcessHandler;
+import org.apache.sling.feature.extension.apiregions.api.ApiRegions;
+
 public class BundleArtifactFeatureHandler extends AbstractHandler implements PostProcessHandler {
     @Override
     public void postProcess(HandlerContext context, Feature feature, Extension extension) {
-        if (!API_REGIONS_NAME.equals(extension.getName()))
+        if (!ApiRegions.EXTENSION_NAME.equals(extension.getName()))
             return;
 
         try {
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/BundleMappingHandler.java b/src/main/java/org/apache/sling/feature/extension/apiregions/BundleMappingHandler.java
index 4b65678..88ca215 100644
--- a/src/main/java/org/apache/sling/feature/extension/apiregions/BundleMappingHandler.java
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/BundleMappingHandler.java
@@ -16,25 +16,25 @@
  */
 package org.apache.sling.feature.extension.apiregions;
 
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Properties;
+import java.util.jar.JarFile;
+
 import org.apache.sling.feature.Artifact;
 import org.apache.sling.feature.Extension;
 import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.builder.HandlerContext;
 import org.apache.sling.feature.builder.PostProcessHandler;
+import org.apache.sling.feature.extension.apiregions.api.ApiRegions;
 import org.apache.sling.feature.io.IOUtils;
 import org.osgi.framework.Constants;
 
-import java.io.File;
-import java.io.IOException;
-import java.net.JarURLConnection;
-import java.net.URL;
-import java.util.Properties;
-import java.util.jar.JarFile;
-
 public class BundleMappingHandler extends AbstractHandler implements PostProcessHandler {
     @Override
     public void postProcess(HandlerContext context, Feature feature, Extension extension) {
-        if (!API_REGIONS_NAME.equals(extension.getName()))
+        if (!ApiRegions.EXTENSION_NAME.equals(extension.getName()))
             return;
 
         try {
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiExport.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiExport.java
new file mode 100644
index 0000000..2fcbd49
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiExport.java
@@ -0,0 +1,105 @@
+/*
+ * 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;
+
+import org.apache.sling.feature.ArtifactId;
+
+/**
+ * Describes an exported package.
+ */
+public class ApiExport {
+
+    private volatile String name;
+
+    private volatile String toggle;
+
+    private volatile ArtifactId previous;
+
+    public ApiExport() {
+        // default
+    }
+
+    public ApiExport(final String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getToggle() {
+        return toggle;
+    }
+
+    public void setToggle(String toggle) {
+        this.toggle = toggle;
+    }
+
+    public ArtifactId getPrevious() {
+        return previous;
+    }
+
+    public void setPrevious(ArtifactId previous) {
+        this.previous = previous;
+    }
+
+    @Override
+    public String toString() {
+        return "ApiExport [name=" + name + ", toggle=" + toggle + ", previous=" + previous + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((name == null) ? 0 : name.hashCode());
+        result = prime * result + ((previous == null) ? 0 : previous.hashCode());
+        result = prime * result + ((toggle == null) ? 0 : toggle.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        ApiExport other = (ApiExport) obj;
+        if (name == null) {
+            if (other.name != null)
+                return false;
+        } else if (!name.equals(other.name))
+            return false;
+        if (previous == null) {
+            if (other.previous != null)
+                return false;
+        } else if (!previous.equals(other.previous))
+            return false;
+        if (toggle == null) {
+            if (other.toggle != null)
+                return false;
+        } else if (!toggle.equals(other.toggle))
+            return false;
+        return true;
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegion.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegion.java
new file mode 100644
index 0000000..d8ef2fa
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegion.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+
+/**
+ * Describes an api region
+ */
+public class ApiRegion {
+
+    private final Collection<ApiExport> exports = new HashSet<>();
+
+    private final Map<String, String> properties = new HashMap<>();
+
+    private volatile String name;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Collection<ApiExport> getExports() {
+        return this.exports;
+    }
+
+    public ApiExport getExportByName(final String name) {
+        for (final ApiExport e : this.exports) {
+            if (e.getName().equals(name)) {
+                return e;
+            }
+        }
+        return null;
+    }
+
+    public Map<String, String> getProperties() {
+        return this.properties;
+    }
+
+    @Override
+    public String toString() {
+        return "ApiRegion [exports=" + exports + ", properties=" + properties + ", name=" + name + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((exports == null) ? 0 : exports.hashCode());
+        result = prime * result + ((name == null) ? 0 : name.hashCode());
+        result = prime * result + ((properties == null) ? 0 : properties.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        ApiRegion other = (ApiRegion) obj;
+        if (exports == null) {
+            if (other.exports != null)
+                return false;
+        } else if (!exports.equals(other.exports))
+            return false;
+        if (name == null) {
+            if (other.name != null)
+                return false;
+        } else if (!name.equals(other.name))
+            return false;
+        if (properties == null) {
+            if (other.properties != null)
+                return false;
+        } else if (!properties.equals(other.properties))
+            return false;
+        return true;
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegions.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegions.java
new file mode 100644
index 0000000..00d939e
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/ApiRegions.java
@@ -0,0 +1,267 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonReader;
+import javax.json.JsonString;
+import javax.json.JsonValue;
+import javax.json.JsonValue.ValueType;
+import javax.json.JsonWriter;
+
+import org.apache.sling.feature.ArtifactId;
+
+/**
+ * An api regions configuration
+ */
+public class ApiRegions {
+
+    /** The name of the api regions extension. */
+    public static final String EXTENSION_NAME = "api-regions";
+
+    private static final String NAME_KEY = "name";
+
+    private static final String EXPORTS_KEY = "exports";
+
+    private static final String TOGGLE_KEY = "toggle";
+
+    private static final String PREVIOUS_KEY = "previous";
+
+    private final List<ApiRegion> regions = new ArrayList<>();
+
+    public List<ApiRegion> getRegions() {
+        return this.regions;
+    }
+
+    /**
+     * Add the region. The region is only added if there isn't already a region with
+     * the same name
+     *
+     * @param region The region to add
+     * @return {@code true} if the region could be added, {@code false} otherwise
+     */
+    public boolean addUniqueRegion(final ApiRegion region) {
+        boolean found = false;
+        for (final ApiRegion c : this.regions) {
+            if (c.getName().equals(region.getName())) {
+                found = true;
+                break;
+            }
+        }
+        if (!found) {
+            this.regions.add(region);
+        }
+        return !found;
+    }
+
+    /**
+     * Get a named region
+     *
+     * @param name The name
+     * @return The region or {@code null}
+     */
+    public ApiRegion getRegionByName(final String name) {
+        ApiRegion found = null;
+
+        for (final ApiRegion c : this.regions) {
+            if (c.getName().equals(name)) {
+                found = c;
+                break;
+            }
+        }
+
+        return found;
+    }
+
+    /**
+     * Get the names of the regions
+     *
+     * @return The list of regions, might be empty
+     */
+    public List<String> getRegionNames() {
+        final List<String> names = new ArrayList<>();
+        for (final ApiRegion c : this.regions) {
+            names.add(c.getName());
+        }
+        return Collections.unmodifiableList(names);
+    }
+
+    /**
+     * Convert regions into json
+     *
+     * @return The json array
+     */
+    public JsonArray toJSONArray() {
+        final JsonArrayBuilder arrayBuilder = Json.createArrayBuilder();
+
+        for (final ApiRegion region : this.getRegions()) {
+            final JsonObjectBuilder regionBuilder = Json.createObjectBuilder();
+            regionBuilder.add(NAME_KEY, region.getName());
+
+            if (!region.getExports().isEmpty()) {
+                final JsonArrayBuilder expArrayBuilder = Json.createArrayBuilder();
+                for (final ApiExport exp : region.getExports()) {
+                    if (exp.getToggle() == null) {
+                        expArrayBuilder.add(exp.getName());
+                    } else {
+                        final JsonObjectBuilder expBuilder = Json.createObjectBuilder();
+                        expBuilder.add(NAME_KEY, exp.getName());
+                        expBuilder.add(TOGGLE_KEY, exp.getToggle());
+                        if (exp.getPrevious() != null) {
+                            expBuilder.add(PREVIOUS_KEY, exp.getPrevious().toMvnId());
+                        }
+                        expArrayBuilder.add(expBuilder);
+                    }
+                }
+
+                regionBuilder.add(EXPORTS_KEY, expArrayBuilder);
+            }
+            for (final Map.Entry<String, String> entry : region.getProperties().entrySet()) {
+                regionBuilder.add(entry.getKey(), entry.getValue());
+            }
+
+            arrayBuilder.add(regionBuilder);
+        }
+
+        return arrayBuilder.build();
+    }
+
+    /**
+     * Convert regions into json
+     *
+     * @return The json array as a string
+     */
+    public String toJSON() {
+        final JsonArray array = this.toJSONArray();
+        try (final StringWriter stringWriter = new StringWriter();
+                final JsonWriter writer = Json.createWriter(stringWriter)) {
+            writer.writeArray(array);
+            return stringWriter.toString();
+        } catch (final IOException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Parse a JSON array into an api regions object
+     *
+     * @param json The json as a string
+     * @return The api regions
+     */
+    public static ApiRegions parse(final String json) {
+        try (final JsonReader reader = Json.createReader(new StringReader(json))) {
+            return parse(reader.readArray());
+        }
+    }
+
+    /**
+     * Parse a JSON array into an api regions object
+     *
+     * @param json The json
+     * @return The api regions
+     */
+    public static ApiRegions parse(final JsonArray json) {
+        final ApiRegions regions = new ApiRegions();
+
+        for (final JsonValue value : json) {
+            if (value.getValueType() != ValueType.OBJECT) {
+                throw new IllegalArgumentException("Illegal api regions json " + json);
+            }
+            final ApiRegion region = new ApiRegion();
+
+            final JsonObject obj = (JsonObject) value;
+            region.setName(obj.getString(NAME_KEY));
+
+            for(final Map.Entry<String, JsonValue> entry : obj.entrySet()) {
+                if ( NAME_KEY.equals(entry.getKey()) ) {
+                    region.setName(obj.getString(NAME_KEY));
+                } else if (entry.getKey().equals(EXPORTS_KEY)) {
+                    for (final JsonValue e : (JsonArray)entry.getValue()) {
+                        if (e.getValueType() == ValueType.STRING) {
+                            final String name = ((JsonString) e).getString();
+                            if (!name.startsWith("#")) {
+                                final ApiExport export = new ApiExport();
+                                region.getExports().add(export);
+
+                                export.setName(name);
+                            }
+                        } else if (e.getValueType() == ValueType.OBJECT) {
+                            final JsonObject expObj = (JsonObject) e;
+                            final ApiExport export = new ApiExport();
+                            region.getExports().add(export);
+
+                            export.setName(expObj.getString(NAME_KEY));
+                            export.setToggle(expObj.getString(TOGGLE_KEY, null));
+                            if (expObj.containsKey(PREVIOUS_KEY)) {
+                                export.setPrevious(ArtifactId.parse(expObj.getString(PREVIOUS_KEY)));
+                            }
+                        }
+                    }
+                } else {
+                    region.getProperties().put(entry.getKey(), ((JsonString) entry.getValue()).getString());
+                }
+            }
+            if (!regions.addUniqueRegion(region)) {
+                throw new IllegalArgumentException("Region " + region.getName() + " is defined twice");
+            }
+
+        }
+        return regions;
+    }
+
+    @Override
+    public String toString() {
+        return "ApiRegions [regions=" + regions + "]";
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((regions == null) ? 0 : regions.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        ApiRegions other = (ApiRegions) obj;
+        if (regions == null) {
+            if (other.regions != null)
+                return false;
+        } else if (!regions.equals(other.regions))
+            return false;
+        return true;
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/extension/apiregions/api/package-info.java b/src/main/java/org/apache/sling/feature/extension/apiregions/api/package-info.java
new file mode 100644
index 0000000..72e56f8
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/extension/apiregions/api/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.sling.feature.extension.apiregions.api;
+
+
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/APIRegionMergeHandlerTest.java b/src/test/java/org/apache/sling/feature/extension/apiregions/APIRegionMergeHandlerTest.java
index fa965e8..aa15510 100644
--- a/src/test/java/org/apache/sling/feature/extension/apiregions/APIRegionMergeHandlerTest.java
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/APIRegionMergeHandlerTest.java
@@ -16,16 +16,9 @@
  */
 package org.apache.sling.feature.extension.apiregions;
 
-import org.apache.sling.feature.Artifact;
-import org.apache.sling.feature.ArtifactId;
-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.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mockito;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -41,9 +34,20 @@ import javax.json.Json;
 import javax.json.JsonArray;
 import javax.json.JsonReader;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.builder.HandlerContext;
+import org.apache.sling.feature.extension.apiregions.api.ApiExport;
+import org.apache.sling.feature.extension.apiregions.api.ApiRegion;
+import org.apache.sling.feature.extension.apiregions.api.ApiRegions;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
 
 public class APIRegionMergeHandlerTest {
     private Path tempDir;
@@ -66,9 +70,9 @@ public class APIRegionMergeHandlerTest {
     public void testCanMerge() {
         APIRegionMergeHandler armh = new APIRegionMergeHandler();
 
-        Extension ex = new Extension(ExtensionType.JSON, "api-regions", false);
+        Extension ex = new Extension(ExtensionType.JSON, "api-regions", ExtensionState.OPTIONAL);
         assertTrue(armh.canMerge(ex));
-        assertFalse(armh.canMerge(new Extension(ExtensionType.JSON, "foo", false)));
+        assertFalse(armh.canMerge(new Extension(ExtensionType.JSON, "foo", ExtensionState.OPTIONAL)));
     }
 
     @Test
@@ -78,14 +82,14 @@ public class APIRegionMergeHandlerTest {
         Feature tf = new Feature(ArtifactId.fromMvnId("x:t:1"));
         Feature sf = new Feature(ArtifactId.fromMvnId("y:s:2"));
 
-        Extension tgEx = new Extension(ExtensionType.JSON, "api-regions", false);
+        Extension tgEx = new Extension(ExtensionType.JSON, "api-regions", ExtensionState.OPTIONAL);
         tgEx.setJSON("[{\"name\":\"global\","
                 + "\"exports\": [\"a.b.c\",\"d.e.f\"]},"
                 + "{\"name\":\"internal\","
                 + "\"exports\":[\"xyz\"],"
                 + "\"some-key\":\"some-val\"}]");
 
-        Extension srEx = new Extension(ExtensionType.JSON, "api-regions", false);
+        Extension srEx = new Extension(ExtensionType.JSON, "api-regions", ExtensionState.OPTIONAL);
         srEx.setJSON("[{\"name\":\"global\","
                 + "\"exports\": [\"test\"]},"
                 + "{\"name\":\"something\","
@@ -95,20 +99,29 @@ public class APIRegionMergeHandlerTest {
         HandlerContext hc = Mockito.mock(HandlerContext.class);
         armh.merge(hc, tf, sf, tgEx, srEx);
 
-        String expectedJSON = "[{\"name\":\"global\","
-                + "\"exports\": [\"a.b.c\",\"d.e.f\", \"test\"]},"
-                + "{\"name\":\"internal\","
-                + "\"exports\":[\"xyz\"],"
-                + "\"some-key\":\"some-val\"},"
-                + "{\"name\":\"something\","
-                + "\"exports\": [\"a.ha\"],"
-                + "\"my-key\": \"my-val\"}]";
-        JsonReader er = Json.createReader(new StringReader(expectedJSON));
-        JsonReader ar = Json.createReader(new StringReader(tgEx.getJSON()));
-        JsonArray ea = er.readArray();
-        JsonArray aa = ar.readArray();
-
-        assertEquals(ea, aa);
+        ApiRegions expected = new ApiRegions();
+        ApiRegion global = new ApiRegion();
+        global.setName("global");
+        global.getExports().add(new ApiExport("a.b.c"));
+        global.getExports().add(new ApiExport("d.e.f"));
+        global.getExports().add(new ApiExport("test"));
+        expected.getRegions().add(global);
+
+        ApiRegion internal = new ApiRegion();
+        internal.setName("internal");
+        internal.getExports().add(new ApiExport("xyz"));
+        internal.getProperties().put("some-key", "some-val");
+        expected.getRegions().add(internal);
+
+        ApiRegion something = new ApiRegion();
+        something.setName("something");
+        something.getExports().add(new ApiExport("a.ha"));
+        something.getProperties().put("my-key", "my-val");
+        expected.getRegions().add(something);
+
+        ApiRegions created = ApiRegions.parse((JsonArray) tgEx.getJSONStructure());
+
+        assertEquals(expected, created);
     }
 
 
@@ -119,7 +132,7 @@ public class APIRegionMergeHandlerTest {
         Feature tf = new Feature(ArtifactId.fromMvnId("x:t:1"));
         Feature sf = new Feature(ArtifactId.fromMvnId("y:s:2"));
 
-        Extension srEx = new Extension(ExtensionType.JSON, "api-regions", false);
+        Extension srEx = new Extension(ExtensionType.JSON, "api-regions", ExtensionState.OPTIONAL);
         srEx.setJSON("[{\"name\":\"global\","
                 + "\"exports\": [\"a.b.c\",\"d.e.f\"]},"
                 + "{\"name\":\"deprecated\","
@@ -157,7 +170,7 @@ public class APIRegionMergeHandlerTest {
 
         Feature tf = new Feature(ArtifactId.fromMvnId("g:t:1"));
         Feature sf1 = new Feature(ArtifactId.fromMvnId("g:s1:1"));
-        Extension sf1Ex = new Extension(ExtensionType.JSON, "api-regions", false);
+        Extension sf1Ex = new Extension(ExtensionType.JSON, "api-regions", ExtensionState.OPTIONAL);
         sf1Ex.setJSON("[]");
 
         sf1.getBundles().add(new Artifact(ArtifactId.fromMvnId("a:b1:1")));
@@ -166,7 +179,7 @@ public class APIRegionMergeHandlerTest {
         armh.merge(hc, tf, sf1, null, sf1Ex);
 
         Feature sf2 = new Feature(ArtifactId.fromMvnId("g:s2:1"));
-        Extension sf2Ex = new Extension(ExtensionType.JSON, "api-regions", false);
+        Extension sf2Ex = new Extension(ExtensionType.JSON, "api-regions", ExtensionState.OPTIONAL);
         sf2Ex.setJSON("[]");
 
         sf2.getBundles().add(new Artifact(ArtifactId.fromMvnId("a:b2:1")));
@@ -176,7 +189,7 @@ public class APIRegionMergeHandlerTest {
         armh.merge(hc, tf, sf2, tf.getExtensions().getByName("api-regions"), sf2Ex);
 
         Feature sf3 = new Feature(ArtifactId.fromMvnId("g:s3:1"));
-        Extension sf3Ex = new Extension(ExtensionType.JSON, "api-regions", false);
+        Extension sf3Ex = new Extension(ExtensionType.JSON, "api-regions", ExtensionState.OPTIONAL);
         sf3Ex.setJSON("[]");
 
         sf3.getBundles().add(new Artifact(ArtifactId.fromMvnId("a:b2:1")));
@@ -204,7 +217,7 @@ public class APIRegionMergeHandlerTest {
         Feature tf = new Feature(ArtifactId.fromMvnId("x:t:1"));
         Feature sf1 = new Feature(ArtifactId.fromMvnId("y:s:2"));
 
-        Extension sr1Ex = new Extension(ExtensionType.JSON, "api-regions", false);
+        Extension sr1Ex = new Extension(ExtensionType.JSON, "api-regions", ExtensionState.OPTIONAL);
         sr1Ex.setJSON("[{\"name\":\"global\","
                 + "\"exports\": [\"a.b.c\",\"d.e.f\"]},"
                 + "{\"name\":\"deprecated\","
@@ -218,7 +231,7 @@ public class APIRegionMergeHandlerTest {
 
         Feature sf2 = new Feature(ArtifactId.fromMvnId("z:s:1"));
 
-        Extension sr2Ex = new Extension(ExtensionType.JSON, "api-regions", false);
+        Extension sr2Ex = new Extension(ExtensionType.JSON, "api-regions", ExtensionState.OPTIONAL);
         sr2Ex.setJSON("[{\"name\":\"global\","
                 + "\"exports\": [\"g.h.i\"]},"
                 + "{\"name\":\"internal\","
diff --git a/src/test/java/org/apache/sling/feature/extension/apiregions/api/TestApiRegions.java b/src/test/java/org/apache/sling/feature/extension/apiregions/api/TestApiRegions.java
new file mode 100644
index 0000000..a03b959
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/extension/apiregions/api/TestApiRegions.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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+
+import org.junit.Test;
+
+public class TestApiRegions {
+
+    private String readJSON(final String name) throws IOException {
+        try (final Reader reader = new InputStreamReader(
+                TestApiRegions.class.getResourceAsStream("/json/" + name + ".json"),
+                "UTF-8"); final Writer writer = new StringWriter()) {
+            int l;
+            char[] buf = new char[2048];
+            while ((l = reader.read(buf)) > -1) {
+                writer.write(buf, 0, l);
+            }
+
+            return writer.toString();
+        }
+    }
+
+    @Test
+    public void testParsing() throws Exception {
+        final String json = readJSON("apis");
+
+        final ApiRegions regions = ApiRegions.parse(json);
+        assertNotNull(regions);
+
+        assertEquals(2, regions.getRegions().size());
+
+        final ApiRegion global = regions.getRegions().get(0);
+        assertEquals("global", global.getName());
+
+        assertEquals(2, global.getExports().size());
+
+        final ApiRegion internal = regions.getRegions().get(1);
+        assertEquals("internal", internal.getName());
+
+        assertEquals(1, internal.getExports().size());
+    }
+}
diff --git a/src/test/resources/json/apis.json b/src/test/resources/json/apis.json
new file mode 100644
index 0000000..eaa81a5
--- /dev/null
+++ b/src/test/resources/json/apis.json
@@ -0,0 +1,15 @@
+[
+   {
+     "name" : "global",
+     "exports" : [
+       "org.apache.sling.global",
+       "org.apache.felix.global"
+     ]
+   },
+   {
+     "name" : "internal",
+     "exports" : [
+       "org.apache.sling.internal"
+     ]
+   }
+]