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 2020/04/19 17:06:14 UTC

[sling-org-apache-sling-feature-io] branch master updated: SLING-9362 : Use Apache Felix cm.json for JSON handling

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-io.git


The following commit(s) were added to refs/heads/master by this push:
     new 974e001  SLING-9362 : Use Apache Felix cm.json for JSON handling
974e001 is described below

commit 974e0018b41c564ca2fb418ccaae51306878819a
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Sun Apr 19 19:06:04 2020 +0200

    SLING-9362 : Use Apache Felix cm.json for JSON handling
---
 bnd.bnd                                            |   4 +-
 pom.xml                                            |  27 +-
 .../feature/io/json/ConfigurationJSONReader.java   |  46 +-
 .../feature/io/json/ConfigurationJSONWriter.java   |  35 +-
 .../sling/feature/io/json/FeatureJSONReader.java   | 645 +++++++++++++++++--
 .../sling/feature/io/json/FeatureJSONWriter.java   | 305 ++++++++-
 .../sling/feature/io/json/JSONReaderBase.java      | 696 ---------------------
 .../sling/feature/io/json/JSONWriterBase.java      | 311 ---------
 .../sling/feature/io/json/ManifestUtils.java       |  23 +-
 .../sling/feature/io/ConfiguratorUtilTest.java     |  27 +-
 .../org/apache/sling/feature/io/IOUtilsTest.java   | 158 ++---
 .../feature/io/artifacts/ArtifactManagerTest.java  |   3 -
 12 files changed, 1076 insertions(+), 1204 deletions(-)

diff --git a/bnd.bnd b/bnd.bnd
index b514d8b..62f82d8 100644
--- a/bnd.bnd
+++ b/bnd.bnd
@@ -1,5 +1,3 @@
--conditionalpackage: org.apache.felix.configurator.impl.json,\\
-                     org.apache.felix.configurator.impl.model,\\
-                     org.apache.felix.utils.collections,\\
+-conditionalpackage: org.apache.felix.utils.collections,\\
                      org.apache.felix.utils.resource,\\
                      org.apache.felix.utils.version
diff --git a/pom.xml b/pom.xml
index 65ad314..3c28398 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,6 +42,13 @@
     <build>
         <plugins>
             <plugin>
+                <groupId>biz.aQute.bnd</groupId>
+                <artifactId>bnd-baseline-maven-plugin</artifactId>
+                <configuration>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+            <plugin>
                 <groupId>org.apache.rat</groupId>
                 <artifactId>apache-rat-plugin</artifactId>
                 <configuration>
@@ -105,31 +112,31 @@
         <dependency>
             <groupId>org.apache.felix</groupId>
             <artifactId>org.apache.felix.converter</artifactId>
-            <version>1.0.8</version>
+            <version>1.0.12</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.felix</groupId>
-            <artifactId>org.apache.felix.configurator</artifactId>
-            <version>1.0.10</version>
+            <artifactId>org.apache.felix.cm.json</artifactId>
+            <version>1.0.1-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.felix</groupId>
             <artifactId>org.apache.felix.utils</artifactId>
-            <version>1.11.2</version>
+            <version>1.11.4</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>
+            <artifactId>geronimo-json_1.1_spec</artifactId>
+            <version>1.2</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.feature</artifactId>
-            <version>1.1.6</version>
+            <version>1.1.8</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
@@ -168,6 +175,12 @@
         <dependency>
             <groupId>org.apache.johnzon</groupId>
             <artifactId>johnzon-core</artifactId>
+            <version>1.2.3</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.util.function</artifactId>
             <version>1.0.0</version>
             <scope>test</scope>
         </dependency>
diff --git a/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONReader.java b/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONReader.java
index d330482..7d4c555 100644
--- a/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONReader.java
+++ b/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONReader.java
@@ -18,20 +18,18 @@ package org.apache.sling.feature.io.json;
 
 import java.io.IOException;
 import java.io.Reader;
-import java.io.StringReader;
-import java.util.Collections;
+import java.util.Hashtable;
 import java.util.Map;
 
-import javax.json.Json;
-import javax.json.JsonObject;
-
-import org.apache.felix.configurator.impl.json.JSONUtil;
+import org.apache.felix.cm.json.ConfigurationReader;
+import org.apache.felix.cm.json.ConfigurationResource;
+import org.apache.sling.feature.Configuration;
 import org.apache.sling.feature.Configurations;
 
 /**
  * JSON Reader for configurations.
  */
-public class ConfigurationJSONReader extends JSONReaderBase {
+public class ConfigurationJSONReader {
 
     /**
      * Read a map of configurations from the reader
@@ -45,32 +43,28 @@ public class ConfigurationJSONReader extends JSONReaderBase {
     public static Configurations read(final Reader reader, final String location)
     throws IOException {
         try {
-            final ConfigurationJSONReader mr = new ConfigurationJSONReader(location);
-            return mr.readConfigurations(reader);
+            final ConfigurationJSONReader mr = new ConfigurationJSONReader();
+            return mr.readConfigurations(location, reader);
         } catch (final IllegalStateException | IllegalArgumentException e) {
             throw new IOException(e);
         }
     }
 
-    /**
-     * Private constructor
-     * @param location Optional location
-     */
-    ConfigurationJSONReader(final String location) {
-        super(location);
-    }
-
-    Configurations readConfigurations(final Reader reader) throws IOException {
+    Configurations readConfigurations(final String location, final Reader reader) throws IOException {
         final Configurations result = new Configurations();
 
-        final JsonObject json = Json.createReader(new StringReader(minify(reader))).readObject();
-
-        @SuppressWarnings("unchecked")
-        final Map<String, Object> map = (Map<String, Object>) JSONUtil.getValue(json);
-
-        final Map<String, Object> objMap = Collections.singletonMap(JSONConstants.FEATURE_CONFIGURATIONS, (Object)map);
-
-        readConfigurations(objMap, result);
+        final ConfigurationReader cfgReader = org.apache.felix.cm.json.Configurations
+            .buildReader()
+            .withIdentifier(location)
+            .verifyAsBundleResource(true)
+            .build(reader);
+        final ConfigurationResource rsrc = cfgReader.readConfigurationResource();
+        for(Map.Entry<String, Hashtable<String, Object>> entry : rsrc.getConfigurations().entrySet() ) {
+            final Configuration cf = new Configuration(entry.getKey());
+            for(final Map.Entry<String, Object> prop : entry.getValue().entrySet()) {
+                cf.getProperties().put(prop.getKey(), prop.getValue());
+            }
+        }
 
         return result;
     }
diff --git a/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONWriter.java b/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONWriter.java
index a6692a4..eb259ba 100644
--- a/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONWriter.java
+++ b/src/main/java/org/apache/sling/feature/io/json/ConfigurationJSONWriter.java
@@ -18,16 +18,19 @@ package org.apache.sling.feature.io.json;
 
 import java.io.IOException;
 import java.io.Writer;
+import java.util.Collections;
+import java.util.Hashtable;
 
-import javax.json.stream.JsonGenerator;
-
+import org.apache.felix.cm.json.ConfigurationResource;
+import org.apache.felix.cm.json.ConfigurationWriter;
+import org.apache.sling.feature.Configuration;
 import org.apache.sling.feature.Configurations;
 
 /** JSON writer for configurations */
-public class ConfigurationJSONWriter extends JSONWriterBase {
+public class ConfigurationJSONWriter {
 
     /** Writes the configurations to the writer. The writer is not closed.
-     * 
+     *
      * @param writer Writer
      * @param configs List of configurations
      * @throws IOException If writing fails */
@@ -39,10 +42,26 @@ public class ConfigurationJSONWriter extends JSONWriterBase {
 
     private void writeConfigurations(final Writer writer, final Configurations configs)
             throws IOException {
-        JsonGenerator generator = newGenerator(writer);
-        writeConfigurations(generator, configs);
-        generator.close();
+
+        final ConfigurationWriter cfgWriter = org.apache.felix.cm.json.Configurations
+            .buildWriter()
+            .build(writer);
+
+        final ConfigurationResource rsrc = new ConfigurationResource();
+        for(final Configuration cfg : configs) {
+            final Hashtable<String, Object> properties;
+            if ( cfg.getProperties() instanceof Hashtable ) {
+                properties = (Hashtable<String, Object>)cfg.getProperties();
+            } else {
+                properties = org.apache.felix.cm.json.Configurations.newConfiguration();
+                for(final String name : Collections.list(cfg.getProperties().keys()) ) {
+                    properties.put(name, cfg.getProperties().get(name));
+                }
+            }
+            rsrc.getConfigurations().put(cfg.getPid(), properties);
+        }
+        cfgWriter.writeConfigurationResource(rsrc);
     }
 
-    
+
 }
diff --git a/src/main/java/org/apache/sling/feature/io/json/FeatureJSONReader.java b/src/main/java/org/apache/sling/feature/io/json/FeatureJSONReader.java
index 54757af..b18a9f4 100644
--- a/src/main/java/org/apache/sling/feature/io/json/FeatureJSONReader.java
+++ b/src/main/java/org/apache/sling/feature/io/json/FeatureJSONReader.java
@@ -18,20 +18,48 @@ package org.apache.sling.feature.io.json;
 
 import java.io.IOException;
 import java.io.Reader;
-import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
 
 import javax.json.Json;
+import javax.json.JsonArray;
 import javax.json.JsonObject;
+import javax.json.JsonString;
+import javax.json.JsonStructure;
+import javax.json.JsonValue;
+import javax.json.JsonValue.ValueType;
 import javax.json.stream.JsonParsingException;
 
+import org.apache.felix.cm.json.ConfigurationReader;
+import org.apache.felix.cm.json.ConfigurationResource;
+import org.apache.felix.utils.resource.CapabilityImpl;
+import org.apache.felix.utils.resource.RequirementImpl;
+import org.apache.sling.feature.Artifact;
 import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Bundles;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Extensions;
 import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.MatchingRequirement;
+import org.apache.sling.feature.Prototype;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Resource;
 
 /**
  * This class offers a method to read a {@code Feature} using a {@code Reader} instance.
  */
-public class FeatureJSONReader extends JSONReaderBase {
+public class FeatureJSONReader {
 
     /**
      * Read a new feature from the reader
@@ -53,14 +81,557 @@ public class FeatureJSONReader extends JSONReaderBase {
     }
 
     /** The read feature. */
-    protected Feature feature;
+    private Feature feature;
+
+    /** The optional location. */
+    private final String location;
+
+    /** Exception prefix containing the location (if set) */
+    private final String exceptionPrefix;
 
     /**
-     * Private constructor
+     * Protected constructor
      * @param location Optional location
      */
     protected FeatureJSONReader(final String location) {
-        super(location);
+        this.location = location;
+        if ( location == null ) {
+            exceptionPrefix = "";
+        } else {
+            exceptionPrefix = location.concat(" : ");
+        }
+    }
+
+    protected ArtifactId getFeatureId(final JsonObject json) throws IOException {
+        if ( !json.containsKey(JSONConstants.FEATURE_ID) ) {
+            throw new IOException(this.exceptionPrefix.concat("Feature id is missing"));
+        }
+        final JsonValue idObj = json.get(JSONConstants.FEATURE_ID);
+        return ArtifactId.parse(checkTypeString(JSONConstants.FEATURE_ID, idObj));
+    }
+
+    private String getProperty(final JsonObject json, final String key) throws IOException {
+        final JsonValue val = json.get(key);
+        if ( val != null ) {
+            return checkTypeString(key, val);
+        }
+        return null;
+    }
+
+    /**
+     * Read the variables section
+     * @param json The json describing the feature or application
+     * @param kvMap The variables will be written to this Key Value Map
+     * @return The same variables as a normal map
+     * @throws IOException If the json is invalid.
+     */
+    private Map<String, String> readVariables(JsonObject json, Map<String,String> kvMap) throws IOException {
+        HashMap<String, String> variables = new HashMap<>();
+
+        if (json.containsKey(JSONConstants.FEATURE_VARIABLES)) {
+            final JsonValue variablesObj = json.get(JSONConstants.FEATURE_VARIABLES);
+
+            for (final Map.Entry<String, JsonValue> entry : checkTypeObject(JSONConstants.FEATURE_VARIABLES, variablesObj).entrySet()) {
+                // skip comments
+                if ( !entry.getKey().startsWith("#") ) {
+                    JsonValue val = entry.getValue();
+                    checkType("variable value", val, ValueType.STRING, ValueType.NUMBER, ValueType.FALSE, ValueType.TRUE, ValueType.NULL, null);
+
+                    String key = entry.getKey();
+                    if (kvMap.get(key) != null) {
+                        throw new IOException(this.exceptionPrefix.concat("Duplicate variable ").concat(key));
+                    }
+
+                    Object convertedVal = org.apache.felix.cm.json.Configurations.convertToObject(val);
+                    String value = convertedVal == null ? null : convertedVal.toString();
+
+                    kvMap.put(key, value);
+                    variables.put(key, value);
+                }
+            }
+        }
+        return variables;
+    }
+
+
+    /**
+     * Read the bundles / start levels section
+     * @param json The json object describing the feature
+     * @param container The bundles container
+     * @param configContainer The configurations container
+     * @throws IOException If the json is invalid.
+     */
+    private void readBundles(
+            final JsonObject json,
+            final Bundles container,
+            final Configurations configContainer) throws IOException {
+        if ( json.containsKey(JSONConstants.FEATURE_BUNDLES)) {
+            final JsonValue bundlesObj = json.get(JSONConstants.FEATURE_BUNDLES);
+            checkType(JSONConstants.FEATURE_BUNDLES, bundlesObj, ValueType.ARRAY);
+
+            final List<Artifact> list = new ArrayList<>();
+            readArtifacts(JSONConstants.FEATURE_BUNDLES, "bundle", list, bundlesObj, configContainer);
+
+            for(final Artifact a : list) {
+                if ( container.containsExact(a.getId())) {
+                    throw new IOException(exceptionPrefix + "Duplicate identical bundle " + a.getId().toMvnId());
+                }
+                try {
+                    // check start order
+                    a.getStartOrder();
+                } catch ( final IllegalArgumentException nfe) {
+                    throw new IOException(exceptionPrefix + "Illegal start order '" + a.getMetadata().get(Artifact.KEY_START_ORDER) + "'");
+                }
+                container.add(a);
+            }
+        }
+    }
+
+    private void readArtifacts(final String section,
+            final String artifactType,
+            final List<Artifact> artifacts,
+            final JsonValue listObj,
+            final Configurations container)
+    throws IOException {
+        checkType(section, listObj, ValueType.ARRAY);
+        for(final JsonValue entry : (JsonArray)listObj) {
+            final Artifact artifact;
+            checkType(artifactType, entry, ValueType.OBJECT, ValueType.STRING);
+            if ( entry.getValueType() == ValueType.STRING ) {
+                final String value = org.apache.felix.cm.json.Configurations.convertToObject(entry).toString();
+                // skip comments
+                if ( value.startsWith("#") ) {
+                    continue;
+                }
+                artifact = new Artifact(ArtifactId.parse(value));
+            } else {
+                final JsonObject bundleObj = (JsonObject) entry;
+                if ( !bundleObj.containsKey(JSONConstants.ARTIFACT_ID) ) {
+                    throw new IOException(exceptionPrefix.concat(" ").concat(artifactType).concat(" is missing required artifact id"));
+                }
+                final String value = checkTypeString(artifactType.concat(" ").concat(JSONConstants.ARTIFACT_ID), bundleObj.get(JSONConstants.ARTIFACT_ID));
+                final ArtifactId id = ArtifactId.parse(value);
+
+                artifact = new Artifact(id);
+                for(final Map.Entry<String, JsonValue> metadataEntry : bundleObj.entrySet()) {
+                    final String key = metadataEntry.getKey();
+                    // skip comments
+                    if ( key.startsWith("#") ) {
+                        continue;
+                    }
+                    if ( JSONConstants.ARTIFACT_KNOWN_PROPERTIES.contains(key) ) {
+                        continue;
+                    }
+                    checkType(artifactType.concat(" metadata ").concat(key), metadataEntry.getValue(), ValueType.STRING, ValueType.FALSE, ValueType.TRUE, ValueType.NUMBER);
+                    final String mval = org.apache.felix.cm.json.Configurations.convertToObject( metadataEntry.getValue()).toString();
+                    artifact.getMetadata().put(key, mval);
+                }
+                if ( bundleObj.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
+                    final JsonObject cfgs = checkTypeObject(artifactType.concat(" configurations"), bundleObj.get(JSONConstants.FEATURE_CONFIGURATIONS));
+                    addConfigurations(cfgs, artifact, container);
+                }
+            }
+            artifacts.add(artifact);
+        }
+    }
+
+    private void addConfigurations(final JsonObject json,
+            final Artifact artifact,
+            final Configurations container) throws IOException {
+        final ConfigurationReader reader = org.apache.felix.cm.json.Configurations.buildReader()
+                .verifyAsBundleResource(true)
+                .withIdentifier(this.location)
+                .build(json);
+        final ConfigurationResource rsrc = reader.readConfigurationResource();
+        if ( !reader.getIgnoredErrors().isEmpty() ) {
+            final StringBuilder builder = new StringBuilder(exceptionPrefix);
+            builder.append("Errors in configurations:");
+            for(final String w : reader.getIgnoredErrors()) {
+                builder.append("\n");
+                builder.append(w);
+            }
+            throw new IOException(builder.toString());
+        }
+
+        for(final Map.Entry<String, Hashtable<String, Object>> c : rsrc.getConfigurations().entrySet()) {
+            final Configuration config = new Configuration(c.getKey());
+
+            for(final Map.Entry<String, Object> prop : c.getValue().entrySet()) {
+                config.getProperties().put(prop.getKey(), prop.getValue());
+            }
+            if ( config.getProperties().get(Configuration.PROP_ARTIFACT_ID) != null ) {
+                throw new IOException(exceptionPrefix.concat("Configuration must not define property ").concat(Configuration.PROP_ARTIFACT_ID));
+            }
+            if ( artifact != null ) {
+                config.getProperties().put(Configuration.PROP_ARTIFACT_ID, artifact.getId().toMvnId());
+            }
+            for(final Configuration current : container) {
+                if ( current.equals(config) ) {
+                    throw new IOException(exceptionPrefix.concat("Duplicate configuration ").concat(config.getPid()));
+                }
+            }
+            container.add(config);
+        }
+    }
+
+
+    private void readConfigurations(final JsonObject json,
+            final Configurations container) throws IOException {
+        if ( json.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
+            final JsonObject cfgs = checkTypeObject(JSONConstants.FEATURE_CONFIGURATIONS, json.get(JSONConstants.FEATURE_CONFIGURATIONS));
+            addConfigurations(cfgs, null, container);
+        }
+    }
+
+    private void readFrameworkProperties(final JsonObject json,
+            final Map<String,String> container) throws IOException {
+        if ( json.containsKey(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES) ) {
+            final JsonValue propsObj= json.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES);
+
+            for(final Map.Entry<String, JsonValue> entry : checkTypeObject(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES, propsObj).entrySet()) {
+                // skip comments
+                if ( entry.getKey().startsWith("#") ) {
+                    continue;
+                }
+                checkType("framework property value", entry.getValue(), ValueType.STRING, ValueType.NUMBER, ValueType.TRUE, ValueType.FALSE);
+                if ( container.get(entry.getKey()) != null ) {
+                    throw new IOException(this.exceptionPrefix.concat("Duplicate framework property ").concat(entry.getKey()));
+                }
+                final String value = org.apache.felix.cm.json.Configurations.convertToObject( entry.getValue()).toString();
+                container.put(entry.getKey(), value);
+            }
+
+        }
+    }
+
+    private void readExtensions(final JsonObject json,
+            final List<String> keywords,
+            final Extensions container,
+            final Configurations configContainer) throws IOException {
+        final Set<String> keySet = new HashSet<>(json.keySet());
+        keySet.removeAll(keywords);
+        // the remaining keys are considered extensions!
+        for(final String key : keySet) {
+            if ( key.startsWith("#") ) {
+                // skip comments
+                continue;
+            }
+            final int pos = key.indexOf(':');
+            final String postfix = pos == -1 ? null : key.substring(pos + 1);
+            final int sep = (postfix == null ? key.indexOf('|') : postfix.indexOf('|'));
+            final String name;
+            final String type;
+            final String state;
+            if ( pos == -1 ) {
+                type = ExtensionType.ARTIFACTS.name();
+                if ( sep == -1 ) {
+                    name = key;
+                    state = ExtensionState.OPTIONAL.name();
+                } else {
+                    name = key.substring(0, sep);
+                    state = key.substring(sep + 1);
+                }
+            } else {
+                name = key.substring(0, pos);
+                if ( sep == -1 ) {
+                    type = postfix;
+                    state = ExtensionState.OPTIONAL.name();
+                } else {
+                    type = postfix.substring(0, sep);
+                    state = postfix.substring(sep + 1);
+                }
+            }
+            if ( JSONConstants.FEATURE_KNOWN_PROPERTIES.contains(name) ) {
+                throw new IOException(this.exceptionPrefix.concat("Extension is using reserved name : ").concat(name));
+            }
+            if ( container.getByName(name) != null ) {
+                throw new IOException(exceptionPrefix.concat("Duplicate extension with name ").concat(name));
+            }
+
+            final ExtensionType extType = ExtensionType.valueOf(type);
+            final ExtensionState extState;
+            if (ExtensionState.OPTIONAL.name().equalsIgnoreCase(state)) {
+                extState = ExtensionState.OPTIONAL;
+            } else if (ExtensionState.REQUIRED.name().equalsIgnoreCase(state)) {
+                extState = ExtensionState.REQUIRED;
+            } else if (ExtensionState.TRANSIENT.name().equalsIgnoreCase(state)) {
+                extState = ExtensionState.TRANSIENT;
+            } else {
+                final boolean opt = Boolean.valueOf(state).booleanValue();
+                extState = opt ? ExtensionState.REQUIRED : ExtensionState.OPTIONAL;
+            }
+
+            final Extension ext = new Extension(extType, name, extState);
+            final JsonValue value = json.get(key);
+            switch ( extType ) {
+                case ARTIFACTS : final List<Artifact> list = new ArrayList<>();
+                                 readArtifacts("Extension ".concat(name), "artifact", list, value, configContainer);
+                                 for(final Artifact a : list) {
+                                     if ( ext.getArtifacts().contains(a) ) {
+                                         throw new IOException(exceptionPrefix.concat("Duplicate artifact in extension ").concat(name).concat(" : ").concat(a.getId().toMvnId()));
+                                     }
+                                     ext.getArtifacts().add(a);
+                                 }
+                                 break;
+                case JSON : checkType("JSON Extension ".concat(name), value, ValueType.OBJECT, ValueType.ARRAY);
+                            ext.setJSONStructure((JsonStructure)value);
+                            break;
+                case TEXT : checkType("Text Extension ".concat(name), value, ValueType.STRING, ValueType.ARRAY);
+                            if ( value.getValueType() == ValueType.STRING ) {
+                                // string
+                                final String textValue = org.apache.felix.cm.json.Configurations.convertToObject(value).toString();
+                                ext.setText(textValue);
+                            } else {
+                                // list (array of strings)
+                                final StringBuilder sb = new StringBuilder();
+                                for(final JsonValue o : ((JsonArray)value)) {
+                                    final String textValue = checkTypeString("Text Extension ".concat(name).concat(", value ").concat(o.toString()), o);
+                                    sb.append(textValue);
+                                    sb.append('\n');
+                                }
+                                ext.setText(sb.toString());
+                            }
+                            break;
+            }
+
+            container.add(ext);
+        }
+    }
+
+    /**
+     * Check if the value is one of the provided types
+     * @param key A key for the error message
+     * @param val The value to check
+     * @param types The allowed types, can also include {@code null} if null is an allowed value.
+     * @throws IOException If the val is not of the specified types
+     */
+    private void checkType(final String key, final JsonValue val, ValueType... types) throws IOException {
+        boolean valid = false;
+        for(ValueType t : types) {
+            if (t == null) {
+                if ( val == null) {
+                    valid = true;
+                    break;
+                }
+            } else if ( val.getValueType() == t ) {
+                valid = true;
+                break;
+            }
+        }
+        if ( !valid ) {
+            throw new IOException(this.exceptionPrefix.concat("Key ").concat(key).concat(" is not one of the allowed types ").concat(Arrays.toString(types)).concat(" : ").concat(val.getValueType().name()));
+        }
+    }
+
+    /**
+     * Check if the value is a string
+     * @param key A key for the error message
+     * @param val The value to check
+     * @return The string value
+     * @throws IOException If the val is not of the specified types
+     */
+    private String checkTypeString(final String key, final JsonValue val) throws IOException {
+        if ( val.getValueType() == ValueType.STRING) {
+            return ((JsonString)val).getString();
+        }
+        throw new IOException(this.exceptionPrefix.concat("Key ").concat(key).concat(" is not of type String : ").concat(val.getValueType().name()));
+    }
+
+    /**
+     * Check if the value is an object
+     * @param key A key for the error message
+     * @param val The value to check
+     * @return The object
+     * @throws IOException If the val is not of the specified types
+     */
+    private JsonObject checkTypeObject(final String key, final JsonValue val) throws IOException {
+        if ( val.getValueType() == ValueType.OBJECT) {
+            return val.asJsonObject();
+        }
+        throw new IOException(this.exceptionPrefix.concat("Key ").concat(key).concat(" is not of type Object : ").concat(val.getValueType().name()));
+    }
+
+    private Prototype readPrototype(final JsonObject json) throws IOException {
+        if ( json.containsKey(JSONConstants.FEATURE_PROTOTYPE)) {
+            final JsonValue prototypeObj = json.get(JSONConstants.FEATURE_PROTOTYPE);
+            checkType(JSONConstants.FEATURE_PROTOTYPE, prototypeObj, ValueType.STRING, ValueType.OBJECT);
+
+            final Prototype prototype;
+            if ( prototypeObj.getValueType() == ValueType.STRING ) {
+                final String textValue = org.apache.felix.cm.json.Configurations.convertToObject(prototypeObj).toString();
+                final ArtifactId id = ArtifactId.parse(textValue);
+                prototype = new Prototype(id);
+            } else {
+                final JsonObject obj = (JsonObject) prototypeObj;
+                if ( !obj.containsKey(JSONConstants.ARTIFACT_ID) ) {
+                    throw new IOException(exceptionPrefix.concat(" prototype is missing required artifact id"));
+                }
+                final String textValue = checkTypeString("Prototype ".concat(JSONConstants.ARTIFACT_ID), obj.get(JSONConstants.ARTIFACT_ID));
+                final ArtifactId id = ArtifactId.parse(textValue);
+                prototype = new Prototype(id);
+
+                if ( obj.containsKey(JSONConstants.PROTOTYPE_REMOVALS) ) {
+                    final JsonObject removalObj = checkTypeObject("Prototype removals", obj.get(JSONConstants.PROTOTYPE_REMOVALS));
+                    if ( removalObj.containsKey(JSONConstants.FEATURE_BUNDLES) ) {
+                        checkType("Prototype removal bundles", removalObj.get(JSONConstants.FEATURE_BUNDLES), ValueType.ARRAY);
+                        for(final JsonValue val : (JsonArray)removalObj.get(JSONConstants.FEATURE_BUNDLES)) {
+                            final String propVal = checkTypeString("Prototype removal bundles", val);
+                            if ( propVal.startsWith("#")) {
+                                continue;
+                            }
+                            prototype.getBundleRemovals().add(ArtifactId.parse(propVal));
+                        }
+                    }
+                    if ( removalObj.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
+                        checkType("Prototype removal configuration", removalObj.get(JSONConstants.FEATURE_CONFIGURATIONS), ValueType.ARRAY);
+                        for(final JsonValue val : (JsonArray)removalObj.get(JSONConstants.FEATURE_CONFIGURATIONS)) {
+                            final String propVal = checkTypeString("Prototype removal configuration", val);
+                            if ( propVal.startsWith("#") ) {
+                                continue;
+                            }
+                            prototype.getConfigurationRemovals().add(propVal);
+                        }
+                    }
+                    if ( removalObj.containsKey(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES) ) {
+                        checkType("Prototype removal framework properties", removalObj.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES), ValueType.ARRAY);
+                        for(final JsonValue val : (JsonArray)removalObj.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES)) {
+                            final String propVal = checkTypeString("Prototype removal framework properties", val);
+                            if ( propVal.startsWith("#") ) {
+                                continue;
+                            }
+                            prototype.getFrameworkPropertiesRemovals().add(propVal);
+                        }
+                    }
+                    if ( removalObj.containsKey(JSONConstants.PROTOTYPE_EXTENSION_REMOVALS) ) {
+                        checkType("Prototype removal extensions", removalObj.get(JSONConstants.PROTOTYPE_EXTENSION_REMOVALS), ValueType.ARRAY);
+                        for(final JsonValue val :  (JsonArray)removalObj.get(JSONConstants.PROTOTYPE_EXTENSION_REMOVALS)) {
+                            checkType("Prototype removal extension", val, ValueType.STRING, ValueType.OBJECT);
+                            if ( val.getValueType() == ValueType.STRING ) {
+                                final String propVal = org.apache.felix.cm.json.Configurations.convertToObject(val).toString();
+                                if ( propVal.startsWith("#")) {
+                                    continue;
+                                }
+                                prototype.getExtensionRemovals().add(propVal);
+                            } else {
+                                final JsonObject removalMap = (JsonObject)val;
+                                final JsonValue nameObj = removalMap.get("name");
+                                final String name = checkTypeString("Prototype removal extension", nameObj);
+                                if ( removalMap.containsKey("artifacts") ) {
+                                    checkType("Prototype removal extension artifacts", removalMap.get("artifacts"), ValueType.ARRAY);
+                                    final List<ArtifactId> ids = new ArrayList<>();
+                                    for(final JsonValue aid : removalMap.getJsonArray("artifacts")) {
+                                        final String propVal = checkTypeString("Prototype removal extension artifact", aid);
+                                        if ( propVal.startsWith("#")) {
+                                            continue;
+                                        }
+                                        ids.add(ArtifactId.parse(propVal));
+                                    }
+                                    prototype.getArtifactExtensionRemovals().put(name, ids);
+                                } else {
+                                    prototype.getExtensionRemovals().add(name);
+                                }
+                            }
+                        }
+                    }
+                    readRequirements(removalObj, prototype.getRequirementRemovals());
+                    readCapabilities(removalObj, prototype.getCapabilityRemovals());
+
+                }
+            }
+            return prototype;
+        }
+        return null;
+    }
+
+    private void readRequirements(final JsonObject json, final List<MatchingRequirement> container)
+            throws IOException {
+        if ( json.containsKey(JSONConstants.FEATURE_REQUIREMENTS)) {
+            final JsonValue reqObj = json.get(JSONConstants.FEATURE_REQUIREMENTS);
+            checkType(JSONConstants.FEATURE_REQUIREMENTS, reqObj, ValueType.ARRAY);
+
+            for(final JsonValue req : ((JsonArray)reqObj)) {
+                final JsonObject obj = checkTypeObject("Requirement", req);
+
+                if ( !obj.containsKey(JSONConstants.REQCAP_NAMESPACE) ) {
+                    throw new IOException(this.exceptionPrefix.concat("Namespace is missing for requirement"));
+                }
+                final String namespace = checkTypeString("Requirement namespace", obj.get(JSONConstants.REQCAP_NAMESPACE));
+
+                Map<String, Object> attrMap = new HashMap<>();
+                if ( obj.containsKey(JSONConstants.REQCAP_ATTRIBUTES) ) {
+                    final JsonObject attrs = checkTypeObject("Requirement attributes", obj.get(JSONConstants.REQCAP_ATTRIBUTES));
+                    attrs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalAttribute(key, value, attrMap::put)));
+                }
+
+                Map<String, String> dirMap = new HashMap<>();
+                if ( obj.containsKey(JSONConstants.REQCAP_DIRECTIVES) ) {
+                    final JsonObject dirs = checkTypeObject("Requirement directives", obj.get(JSONConstants.REQCAP_DIRECTIVES));
+                    dirs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalDirective(key, value, dirMap::put)));
+                }
+
+                final MatchingRequirement r = new MatchingRequirementImpl(null,
+                        namespace, dirMap, attrMap);
+                container.add(r);
+            }
+        }
+    }
+
+    private void readCapabilities(final JsonObject json, final List<Capability> container) throws IOException {
+        if ( json.containsKey(JSONConstants.FEATURE_CAPABILITIES)) {
+            final JsonValue capObj = json.get(JSONConstants.FEATURE_CAPABILITIES);
+            checkType(JSONConstants.FEATURE_CAPABILITIES, capObj, ValueType.ARRAY);
+
+            for(final JsonValue cap : ((JsonArray)capObj)) {
+                final JsonObject obj = checkTypeObject("Capability", cap);
+
+                if ( !obj.containsKey(JSONConstants.REQCAP_NAMESPACE) ) {
+                    throw new IOException(this.exceptionPrefix.concat("Namespace is missing for capability"));
+                }
+                final String namespace = checkTypeString("Capability namespace", obj.get(JSONConstants.REQCAP_NAMESPACE));
+
+                Map<String, Object> attrMap = new HashMap<>();
+                if ( obj.containsKey(JSONConstants.REQCAP_ATTRIBUTES) ) {
+                    final JsonObject attrs = checkTypeObject("Capability attributes", obj.get(JSONConstants.REQCAP_ATTRIBUTES));
+                    attrs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalAttribute(key, value, attrMap::put)));
+                }
+
+                Map<String, String> dirMap = new HashMap<>();
+                if ( obj.containsKey(JSONConstants.REQCAP_DIRECTIVES) ) {
+                    final JsonObject dirs = checkTypeObject("Capability directives", obj.get(JSONConstants.REQCAP_DIRECTIVES));
+                    dirs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalDirective(key, value, dirMap::put)));
+                }
+
+                final Capability c = new CapabilityImpl(null, namespace, dirMap, attrMap);
+                container.add(c);
+            }
+        }
+    }
+
+    @FunctionalInterface
+    private interface BiConsumer_WithExceptions<T, V, E extends Exception> {
+        void accept(T t, V u) throws E;
+    }
+
+    private static <T, V, E extends Exception> BiConsumer<T, V> rethrowBiConsumer(BiConsumer_WithExceptions<T, V, E> biConsumer) {
+        return (t, u) -> {
+            try {
+                biConsumer.accept(t, u);
+            } catch (Exception exception) {
+                throwAsUnchecked(exception);
+            }
+        };
+    }
+
+    @SuppressWarnings ("unchecked")
+    private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E {
+        throw (E) exception;
+    }
+
+    private static class MatchingRequirementImpl extends RequirementImpl implements MatchingRequirement {
+
+        public MatchingRequirementImpl(Resource res, String ns, Map<String, String> dirs, Map<String, Object> attrs) {
+            super(res, ns, dirs, attrs);
+        }
     }
 
     /**
@@ -69,70 +640,60 @@ public class FeatureJSONReader extends JSONReaderBase {
      * @return The feature object
      * @throws IOException If an IO error occurs or the JSON is not valid.
      */
-    protected Feature readFeature(final Reader reader)
+    private Feature readFeature(final Reader reader)
     throws IOException {
-        final JsonObject json = Json.createReader(new StringReader(minify(reader))).readObject();
-        final Map<String, Object> map = getJsonMap(json);
+        final JsonObject json = Json.createReader(org.apache.felix.cm.json.Configurations.jsonCommentAwareReader(reader)).readObject();
 
-        checkModelVersion(map);
+        checkModelVersion(json);
 
-        final ArtifactId featureId = this.getFeatureId(map);
+        final ArtifactId featureId = this.getFeatureId(json);
         this.feature = new Feature(featureId);
         this.feature.setLocation(this.location);
 
         // final flag
-        if (map.containsKey(JSONConstants.FEATURE_FINAL)) {
-            final Object finalObj = map.get(JSONConstants.FEATURE_FINAL);
-            checkType(JSONConstants.FEATURE_FINAL, finalObj, Boolean.class);
-            this.feature.setFinal(((Boolean) finalObj).booleanValue());
+        if (json.containsKey(JSONConstants.FEATURE_FINAL)) {
+            final JsonValue finalObj = json.get(JSONConstants.FEATURE_FINAL);
+            checkType(JSONConstants.FEATURE_FINAL, finalObj, JsonValue.ValueType.FALSE, JsonValue.ValueType.TRUE);
+            this.feature.setFinal(((Boolean)org.apache.felix.cm.json.Configurations.convertToObject(finalObj)).booleanValue());
         }
 
         // complete flag
-        if (map.containsKey(JSONConstants.FEATURE_COMPLETE)) {
-            final Object completeObj = map.get(JSONConstants.FEATURE_COMPLETE);
-            checkType(JSONConstants.FEATURE_COMPLETE, completeObj, Boolean.class);
-            this.feature.setComplete(((Boolean) completeObj).booleanValue());
+        if (json.containsKey(JSONConstants.FEATURE_COMPLETE)) {
+            final JsonValue completeObj = json.get(JSONConstants.FEATURE_COMPLETE);
+            checkType(JSONConstants.FEATURE_COMPLETE, completeObj, JsonValue.ValueType.FALSE, JsonValue.ValueType.TRUE);
+            this.feature.setComplete(((Boolean)org.apache.felix.cm.json.Configurations.convertToObject(completeObj)).booleanValue());
         }
 
         // title, description, vendor and license
-        this.feature.setTitle(getProperty(map, JSONConstants.FEATURE_TITLE));
-        this.feature.setDescription(getProperty(map, JSONConstants.FEATURE_DESCRIPTION));
-        this.feature.setVendor(getProperty(map, JSONConstants.FEATURE_VENDOR));
-        this.feature.setLicense(getProperty(map, JSONConstants.FEATURE_LICENSE));
+        this.feature.setTitle(getProperty(json, JSONConstants.FEATURE_TITLE));
+        this.feature.setDescription(getProperty(json, JSONConstants.FEATURE_DESCRIPTION));
+        this.feature.setVendor(getProperty(json, JSONConstants.FEATURE_VENDOR));
+        this.feature.setLicense(getProperty(json, JSONConstants.FEATURE_LICENSE));
 
-        this.readVariables(map, feature.getVariables());
-        this.readBundles(map, feature.getBundles(), feature.getConfigurations());
-        this.readFrameworkProperties(map, feature.getFrameworkProperties());
-        this.readConfigurations(map, feature.getConfigurations());
+        this.readVariables(json, feature.getVariables());
+        this.readBundles(json, feature.getBundles(), feature.getConfigurations());
+        this.readFrameworkProperties(json, feature.getFrameworkProperties());
+        this.readConfigurations(json, feature.getConfigurations());
 
-        this.readCapabilities(map, feature.getCapabilities());
-        this.readRequirements(map, feature.getRequirements());
-        feature.setPrototype(this.readPrototype(map));
+        this.readCapabilities(json, feature.getCapabilities());
+        this.readRequirements(json, feature.getRequirements());
+        feature.setPrototype(this.readPrototype(json));
 
-        this.readExtensions(map,
+        this.readExtensions(json,
                 JSONConstants.FEATURE_KNOWN_PROPERTIES,
                 this.feature.getExtensions(), this.feature.getConfigurations());
 
         return feature;
     }
 
-    private void checkModelVersion(final Map<String, Object> map) throws IOException {
-        String modelVersion = getProperty(map, JSONConstants.FEATURE_MODEL_VERSION);
+    private void checkModelVersion(final JsonObject json) throws IOException {
+        String modelVersion = getProperty(json, JSONConstants.FEATURE_MODEL_VERSION);
         if (modelVersion == null) {
             modelVersion = "1";
         }
         if (!"1".equals(modelVersion)) {
-            throw new IOException("Unsupported model version: " + modelVersion);
-        }
-    }
-
-    protected ArtifactId getFeatureId(final Map<String, Object> map) throws IOException {
-        if ( !map.containsKey(JSONConstants.FEATURE_ID) ) {
-            throw new IOException(this.exceptionPrefix + "Feature id is missing");
+            throw new IOException(this.exceptionPrefix.concat("Unsupported model version: ").concat(modelVersion));
         }
-        final Object idObj = map.get(JSONConstants.FEATURE_ID);
-        checkType(JSONConstants.FEATURE_ID, idObj, String.class);
-        return ArtifactId.parse(idObj.toString());
     }
 }
 
diff --git a/src/main/java/org/apache/sling/feature/io/json/FeatureJSONWriter.java b/src/main/java/org/apache/sling/feature/io/json/FeatureJSONWriter.java
index af36576..029a73b 100644
--- a/src/main/java/org/apache/sling/feature/io/json/FeatureJSONWriter.java
+++ b/src/main/java/org/apache/sling/feature/io/json/FeatureJSONWriter.java
@@ -16,19 +16,38 @@
  */
 package org.apache.sling.feature.io.json;
 
+import java.io.FilterWriter;
 import java.io.IOException;
 import java.io.Writer;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
 
+import javax.json.Json;
 import javax.json.stream.JsonGenerator;
+import javax.json.stream.JsonGeneratorFactory;
 
+import org.apache.felix.cm.json.ConfigurationResource;
+import org.apache.felix.cm.json.ConfigurationWriter;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Bundles;
 import org.apache.sling.feature.Configuration;
 import org.apache.sling.feature.Configurations;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
 import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.MatchingRequirement;
+import org.apache.sling.feature.Prototype;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
 
 /**
  * Simple JSON writer for a feature
  */
-public class FeatureJSONWriter extends JSONWriterBase {
+public class FeatureJSONWriter {
 
     /**
      * Writes the feature to the writer.
@@ -47,6 +66,287 @@ public class FeatureJSONWriter extends JSONWriterBase {
     	// protected constructor for subclassing
     }
 
+    private final JsonGeneratorFactory generatorFactory = Json.createGeneratorFactory(Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true));
+
+    private final JsonGenerator newGenerator(final Writer writer) {
+        // prevent closing of the underlying writer
+        return generatorFactory.createGenerator(new FilterWriter(writer) {
+
+            @Override
+            public void close() throws IOException {
+                super.flush();
+            }
+        });
+    }
+
+    private void writeBundles(final JsonGenerator generator,
+            final Bundles bundles,
+            final Configurations allConfigs) {
+        // bundles
+        if ( !bundles.isEmpty() ) {
+            generator.writeStartArray(JSONConstants.FEATURE_BUNDLES);
+
+            for(final Artifact artifact : bundles) {
+                final Configurations cfgs = new Configurations();
+                for(final Configuration cfg : allConfigs) {
+                    final String artifactProp = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT_ID);
+                    if ( artifact.getId().toMvnId().equals(artifactProp) ) {
+                        cfgs.add(cfg);
+                    }
+                }
+                Map<String,String> md = artifact.getMetadata();
+                if ( md.isEmpty() && cfgs.isEmpty() ) {
+                    generator.write(artifact.getId().toMvnId());
+                } else {
+                    generator.writeStartObject();
+                    generator.write(JSONConstants.ARTIFACT_ID, artifact.getId().toMvnId());
+
+                    Object runmodes = md.remove("runmodes");
+                    if (runmodes instanceof String) {
+                        md.put("run-modes", (String) runmodes);
+                    }
+
+                    for(final Map.Entry<String, String> me : md.entrySet()) {
+                        generator.write(me.getKey(), me.getValue());
+                    }
+
+                    generator.writeEnd();
+                }
+            }
+
+            generator.writeEnd();
+        }
+    }
+
+    /**
+     * Write the list of configurations into a "configurations" element
+     * @param generator The json generator
+     * @param cfgs The list of configurations
+     * @throws IOException
+     */
+    private void writeConfigurations(final JsonGenerator generator, final Configurations cfgs) throws IOException {
+        if ( cfgs.isEmpty() ) {
+            return;
+        }
+
+        generator.writeKey(JSONConstants.FEATURE_CONFIGURATIONS);
+
+        final ConfigurationWriter cfgWriter = org.apache.felix.cm.json.Configurations
+                .buildWriter()
+                .build(generator);
+
+        final ConfigurationResource rsrc = new ConfigurationResource();
+        for(final Configuration cfg : cfgs) {
+            final Hashtable<String, Object> properties;
+            if ( cfg.getProperties() instanceof Hashtable ) {
+                properties = (Hashtable<String, Object>)cfg.getProperties();
+            } else {
+                properties = org.apache.felix.cm.json.Configurations.newConfiguration();
+                for(final String name : Collections.list(cfg.getProperties().keys()) ) {
+                    properties.put(name, cfg.getProperties().get(name));
+                }
+            }
+            rsrc.getConfigurations().put(cfg.getPid(), properties);
+        }
+        cfgWriter.writeConfigurationResource(rsrc);
+    }
+
+    private void writeVariables(final JsonGenerator generator, final Map<String,String> vars) {
+        if ( !vars.isEmpty()) {
+            generator.writeStartObject(JSONConstants.FEATURE_VARIABLES);
+
+            for (final Map.Entry<String, String> entry : vars.entrySet()) {
+                String val = entry.getValue();
+                if (val != null)
+                    generator.write(entry.getKey(), val);
+                else
+                    generator.writeNull(entry.getKey());
+            }
+
+            generator.writeEnd();
+        }
+    }
+
+    private void writeFrameworkProperties(final JsonGenerator generator, final Map<String,String> props) {
+        // framework properties
+        if ( !props.isEmpty() ) {
+            generator.writeStartObject(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES);
+            for(final Map.Entry<String, String> entry : props.entrySet()) {
+                generator.write(entry.getKey(), entry.getValue());
+            }
+            generator.writeEnd();
+        }
+    }
+
+    private void writeExtensions(final JsonGenerator generator,
+            final List<Extension> extensions,
+            final Configurations allConfigs) throws IOException {
+        for(final Extension ext : extensions) {
+            final String state;
+            switch (ext.getState()) {
+            case OPTIONAL:
+                state = "false";
+                break;
+            case REQUIRED:
+                state = "true";
+                break;
+            default:
+                state = ext.getState().name();
+            }
+            final String key = ext.getName().concat(":").concat(ext.getType().name()).concat("|").concat(state);
+            if ( ext.getType() == ExtensionType.JSON ) {
+                generator.write(key, ext.getJSONStructure());
+            } else if ( ext.getType() == ExtensionType.TEXT ) {
+                generator.writeStartArray(key);
+                for(String line : ext.getText().split("\n")) {
+                    generator.write(line);
+                }
+                generator.writeEnd();
+            } else {
+                generator.writeStartArray(key);
+                for(final Artifact artifact : ext.getArtifacts()) {
+                    final Configurations artifactCfgs = new Configurations();
+                    for(final Configuration cfg : allConfigs) {
+                        final String artifactProp = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT_ID);
+                        if (  artifact.getId().toMvnId().equals(artifactProp) ) {
+                            artifactCfgs.add(cfg);
+                        }
+                    }
+                    if ( artifact.getMetadata().isEmpty() && artifactCfgs.isEmpty() ) {
+                        generator.write(artifact.getId().toMvnId());
+                    } else {
+                        generator.writeStartObject();
+                        generator.write(JSONConstants.ARTIFACT_ID, artifact.getId().toMvnId());
+
+                        for(final Map.Entry<String, String> me : artifact.getMetadata().entrySet()) {
+                            generator.write(me.getKey(), me.getValue());
+                        }
+
+                        writeConfigurations(generator, artifactCfgs);
+
+                        generator.writeEnd();
+                    }
+                }
+                generator.writeEnd();
+            }
+        }
+    }
+
+    private void writeProperty(final JsonGenerator generator, final String key, final String value) {
+        if ( value != null ) {
+            generator.write(key, value);
+        }
+    }
+
+    private <T> void writeList(final JsonGenerator generator, final String name, final Collection<T> values) {
+        if (!values.isEmpty()) {
+            generator.writeStartArray(name);
+            for (T value : values) {
+                generator.write(value.toString());
+            }
+            generator.writeEnd();
+        }
+    }
+
+    private void writePrototype(final JsonGenerator generator, final Prototype inc) {
+        if (inc == null) {
+            return;
+        }
+
+        if ( inc.getArtifactExtensionRemovals().isEmpty()
+             && inc.getBundleRemovals().isEmpty()
+             && inc.getConfigurationRemovals().isEmpty()
+             && inc.getFrameworkPropertiesRemovals().isEmpty()
+             && inc.getRequirementRemovals().isEmpty()
+             && inc.getCapabilityRemovals().isEmpty() ) {
+
+            generator.write(JSONConstants.FEATURE_PROTOTYPE, inc.getId().toMvnId());
+        } else {
+            generator.writeStartObject(JSONConstants.FEATURE_PROTOTYPE);
+            writeProperty(generator, JSONConstants.ARTIFACT_ID, inc.getId().toMvnId());
+
+            generator.writeStartObject(JSONConstants.PROTOTYPE_REMOVALS);
+
+            if ( !inc.getArtifactExtensionRemovals().isEmpty()
+                 || inc.getExtensionRemovals().isEmpty() ) {
+                generator.writeStartArray(JSONConstants.PROTOTYPE_EXTENSION_REMOVALS);
+
+                for(final String id : inc.getExtensionRemovals()) {
+                    generator.write(id);
+                }
+                for(final Map.Entry<String, List<ArtifactId>> entry : inc.getArtifactExtensionRemovals().entrySet()) {
+                    generator.writeStartObject();
+
+                    writeList(generator, entry.getKey(), entry.getValue());
+
+                    generator.writeEnd();
+                }
+
+                generator.writeEnd();
+            }
+            writeList(generator, JSONConstants.FEATURE_CONFIGURATIONS, inc.getConfigurationRemovals());
+            writeList(generator, JSONConstants.FEATURE_BUNDLES, inc.getBundleRemovals());
+            writeList(generator, JSONConstants.FEATURE_FRAMEWORK_PROPERTIES, inc.getFrameworkPropertiesRemovals());
+
+            writeRequirements(generator, inc.getRequirementRemovals());
+            writeCapabilities(generator, inc.getCapabilityRemovals());
+
+            generator.writeEnd().writeEnd();
+        }
+    }
+
+    private void writeRequirements(final JsonGenerator generator, final List<MatchingRequirement> requirements) {
+        if (requirements.isEmpty()) {
+            return;
+        }
+
+        generator.writeStartArray(JSONConstants.FEATURE_REQUIREMENTS);
+
+        for(final Requirement req : requirements) {
+            generator.writeStartObject();
+            writeProperty(generator, JSONConstants.REQCAP_NAMESPACE, req.getNamespace());
+            if ( !req.getAttributes().isEmpty() ) {
+                generator.writeStartObject(JSONConstants.REQCAP_ATTRIBUTES);
+                req.getAttributes().forEach((key, value) -> ManifestUtils.marshalAttribute(key, value, generator::write));
+                generator.writeEnd();
+            }
+            if ( !req.getDirectives().isEmpty() ) {
+                generator.writeStartObject(JSONConstants.REQCAP_DIRECTIVES);
+                req.getDirectives().forEach((key, value) -> ManifestUtils.marshalDirective(key, value, generator::write));
+                generator.writeEnd();
+            }
+            generator.writeEnd();
+        }
+
+        generator.writeEnd();
+    }
+
+    private void writeCapabilities(final JsonGenerator generator, final List<Capability> capabilities) {
+        if (capabilities.isEmpty()) {
+            return;
+        }
+
+        generator.writeStartArray(JSONConstants.FEATURE_CAPABILITIES);
+
+        for(final Capability cap : capabilities) {
+            generator.writeStartObject();
+            writeProperty(generator, JSONConstants.REQCAP_NAMESPACE, cap.getNamespace());
+            if ( !cap.getAttributes().isEmpty() ) {
+                generator.writeStartObject(JSONConstants.REQCAP_ATTRIBUTES);
+                cap.getAttributes().forEach((key, value) -> ManifestUtils.marshalAttribute(key, value, generator::write));
+                generator.writeEnd();
+            }
+            if ( !cap.getDirectives().isEmpty() ) {
+                generator.writeStartObject(JSONConstants.REQCAP_DIRECTIVES);
+                cap.getDirectives().forEach((key, value) -> ManifestUtils.marshalDirective(key, value, generator::write));
+                generator.writeEnd();
+            }
+            generator.writeEnd();
+        }
+
+        generator.writeEnd();
+    }
+
     /**
      * Writes the feature to the writer.
      * The writer is not closed.
@@ -54,7 +354,7 @@ public class FeatureJSONWriter extends JSONWriterBase {
      * @param feature Feature
      * @throws IOException If writing fails
      */
-    protected void writeFeature(final Writer writer, final Feature feature)
+    private void writeFeature(final Writer writer, final Feature feature)
     throws IOException {
         JsonGenerator generator = newGenerator(writer);
         generator.writeStartObject();
@@ -113,4 +413,5 @@ public class FeatureJSONWriter extends JSONWriterBase {
     		final Feature feature) {
         writeProperty(generator, JSONConstants.FEATURE_ID, feature.getId().toMvnId());
     }
+
 }
diff --git a/src/main/java/org/apache/sling/feature/io/json/JSONReaderBase.java b/src/main/java/org/apache/sling/feature/io/json/JSONReaderBase.java
deleted file mode 100644
index 679b1f1..0000000
--- a/src/main/java/org/apache/sling/feature/io/json/JSONReaderBase.java
+++ /dev/null
@@ -1,696 +0,0 @@
-/*
- * 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.io.json;
-
-import java.io.IOException;
-import java.io.Reader;
-import java.io.StringWriter;
-import java.io.Writer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.function.BiConsumer;
-
-import javax.json.Json;
-import javax.json.JsonArrayBuilder;
-import javax.json.JsonObject;
-import javax.json.JsonObjectBuilder;
-import javax.json.JsonStructure;
-
-import org.apache.felix.configurator.impl.json.JSMin;
-import org.apache.felix.configurator.impl.json.JSONUtil;
-import org.apache.felix.configurator.impl.json.TypeConverter;
-import org.apache.felix.configurator.impl.model.Config;
-import org.apache.felix.utils.resource.CapabilityImpl;
-import org.apache.felix.utils.resource.RequirementImpl;
-import org.apache.sling.feature.Artifact;
-import org.apache.sling.feature.ArtifactId;
-import org.apache.sling.feature.Bundles;
-import org.apache.sling.feature.Configuration;
-import org.apache.sling.feature.Configurations;
-import org.apache.sling.feature.Extension;
-import org.apache.sling.feature.ExtensionState;
-import org.apache.sling.feature.ExtensionType;
-import org.apache.sling.feature.Extensions;
-import org.apache.sling.feature.MatchingRequirement;
-import org.apache.sling.feature.Prototype;
-import org.osgi.resource.Capability;
-import org.osgi.resource.Resource;
-
-/**
- * Common methods for JSON reading.
- */
-abstract class JSONReaderBase {
-
-    /** The optional location. */
-    protected final String location;
-
-    /** Exception prefix containing the location (if set) */
-    protected final String exceptionPrefix;
-
-    /**
-     * Private constructor
-     * @param location Optional location
-     */
-    JSONReaderBase(final String location) {
-        this.location = location;
-        if ( location == null ) {
-            exceptionPrefix = "";
-        } else {
-            exceptionPrefix = location + " : ";
-        }
-    }
-
-    protected String minify(final Reader reader) throws IOException {
-       // minify JSON (remove comments)
-        final String contents;
-        try ( final Writer out = new StringWriter()) {
-            final JSMin min = new JSMin(reader, out);
-            min.jsmin();
-            contents = out.toString();
-        }
-        return contents;
-    }
-
-    /**
-     * Get the JSON object as a map, removing all comments that start with a '#' character
-     * @param json The JSON object to process
-     * @return A map representing the JSON object.
-     */
-    protected Map<String, Object> getJsonMap(JsonObject json) {
-        @SuppressWarnings("unchecked")
-        Map<String, Object> m = (Map<String, Object>) JSONUtil.getValue(json);
-
-        removeComments(m);
-        return m;
-    }
-
-    private void removeComments(Map<String, Object> m) {
-        for(Iterator<Map.Entry<String, Object>> it = m.entrySet().iterator(); it.hasNext(); ) {
-            Entry<String, ?> entry = it.next();
-            if (entry.getKey().startsWith("#")) {
-                it.remove();
-            } else if (entry.getValue() instanceof Map) {
-                @SuppressWarnings("unchecked")
-                Map<String, Object> embedded = (Map<String, Object>) entry.getValue();
-                removeComments(embedded);
-            } else if (entry.getValue() instanceof Collection) {
-                Collection<?> embedded = (Collection<?>) entry.getValue();
-                removeComments(embedded);
-            }
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    private void removeComments(Collection<?> embedded) {
-        for (Object el : embedded) {
-            if (el instanceof Collection) {
-                removeComments((Collection<?>) el);
-            } else if (el instanceof Map) {
-                removeComments((Map<String, Object>) el);
-            }
-        }
-    }
-
-    protected String getProperty(final Map<String, Object> map, final String key) throws IOException {
-        final Object val = map.get(key);
-        if ( val != null ) {
-            checkType(key, val, String.class);
-            return val.toString();
-        }
-        return null;
-    }
-
-    /**
-     * Read the variables section
-     * @param map The map describing the feature or application
-     * @param kvMap The variables will be written to this Key Value Map
-     * @return The same variables as a normal map
-     * @throws IOException If the json is invalid.
-     */
-    protected Map<String, String> readVariables(Map<String, Object> map, Map<String,String> kvMap) throws IOException {
-        HashMap<String, String> variables = new HashMap<>();
-
-        if (map.containsKey(JSONConstants.FEATURE_VARIABLES)) {
-            final Object variablesObj = map.get(JSONConstants.FEATURE_VARIABLES);
-            checkType(JSONConstants.FEATURE_VARIABLES, variablesObj, Map.class);
-
-            @SuppressWarnings("unchecked")
-            final Map<String, Object> vars = (Map<String, Object>) variablesObj;
-            for (final Map.Entry<String, Object> entry : vars.entrySet()) {
-                Object val = entry.getValue();
-                checkType("variable value", val, String.class, Boolean.class, Number.class, null);
-
-                String key = entry.getKey();
-                if (kvMap.get(key) != null) {
-                    throw new IOException(this.exceptionPrefix + "Duplicate variable " + key);
-                }
-
-                String value = val == null ? null : val.toString();
-
-                kvMap.put(key, value);
-                variables.put(key, value);
-            }
-        }
-        return variables;
-    }
-
-
-    /**
-     * Read the bundles / start levels section
-     * @param map The map describing the feature
-     * @param container The bundles container
-     * @param configContainer The configurations container
-     * @throws IOException If the json is invalid.
-     */
-    protected void readBundles(
-            final Map<String, Object> map,
-            final Bundles container,
-            final Configurations configContainer) throws IOException {
-        if ( map.containsKey(JSONConstants.FEATURE_BUNDLES)) {
-            final Object bundlesObj = map.get(JSONConstants.FEATURE_BUNDLES);
-            checkType(JSONConstants.FEATURE_BUNDLES, bundlesObj, List.class);
-
-            final List<Artifact> list = new ArrayList<>();
-            readArtifacts(JSONConstants.FEATURE_BUNDLES, "bundle", list, bundlesObj, configContainer);
-
-            for(final Artifact a : list) {
-                if ( container.containsExact(a.getId())) {
-                    throw new IOException(exceptionPrefix + "Duplicate identical bundle " + a.getId().toMvnId());
-                }
-                try {
-                    // check start order
-                    a.getStartOrder();
-                } catch ( final IllegalArgumentException nfe) {
-                    throw new IOException(exceptionPrefix + "Illegal start order '" + a.getMetadata().get(Artifact.KEY_START_ORDER) + "'");
-                }
-                container.add(a);
-            }
-        }
-    }
-
-    protected void readArtifacts(final String section,
-            final String artifactType,
-            final List<Artifact> artifacts,
-            final Object listObj,
-            final Configurations container)
-    throws IOException {
-        checkType(section, listObj, List.class);
-        @SuppressWarnings("unchecked")
-        final List<Object> list = (List<Object>) listObj;
-        for(final Object entry : list) {
-            final Artifact artifact;
-            checkType(artifactType, entry, Map.class, String.class);
-            if ( entry instanceof String ) {
-                // skip comments
-                if ( entry.toString().startsWith("#") ) {
-                    continue;
-                }
-                artifact = new Artifact(ArtifactId.parse((String) entry));
-            } else {
-                @SuppressWarnings("unchecked")
-                final Map<String, Object> bundleObj = (Map<String, Object>) entry;
-                if ( !bundleObj.containsKey(JSONConstants.ARTIFACT_ID) ) {
-                    throw new IOException(exceptionPrefix + " " + artifactType + " is missing required artifact id");
-                }
-                checkType(artifactType + " " + JSONConstants.ARTIFACT_ID, bundleObj.get(JSONConstants.ARTIFACT_ID), String.class);
-                final ArtifactId id = ArtifactId.parse(bundleObj.get(JSONConstants.ARTIFACT_ID).toString());
-
-                artifact = new Artifact(id);
-                for(final Map.Entry<String, Object> metadataEntry : bundleObj.entrySet()) {
-                    final String key = metadataEntry.getKey();
-                    if ( JSONConstants.ARTIFACT_KNOWN_PROPERTIES.contains(key) ) {
-                        continue;
-                    }
-                    checkType(artifactType + " metadata " + key, metadataEntry.getValue(), String.class, Number.class, Boolean.class);
-                    artifact.getMetadata().put(key, metadataEntry.getValue().toString());
-                }
-                if ( bundleObj.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
-                    checkType(artifactType + " configurations", bundleObj.get(JSONConstants.FEATURE_CONFIGURATIONS), Map.class);
-                    addConfigurations(bundleObj, artifact, container);
-                }
-            }
-            artifacts.add(artifact);
-        }
-    }
-
-    protected void addConfigurations(final Map<String, Object> map,
-            final Artifact artifact,
-            final Configurations container) throws IOException {
-        final JSONUtil.Report report = new JSONUtil.Report();
-        @SuppressWarnings("unchecked")
-        final List<Config> configs = JSONUtil.readConfigurationsJSON(new TypeConverter(null),
-                0, "", (Map<String, ?>)map.get(JSONConstants.FEATURE_CONFIGURATIONS), report);
-        if ( !report.errors.isEmpty() || !report.warnings.isEmpty() ) {
-            final StringBuilder builder = new StringBuilder(exceptionPrefix);
-            builder.append("Errors in configurations:");
-            for(final String w : report.warnings) {
-                builder.append("\n");
-                builder.append(w);
-            }
-            for(final String e : report.errors) {
-                builder.append("\n");
-                builder.append(e);
-            }
-            throw new IOException(builder.toString());
-        }
-
-        for(final Config c : configs) {
-            final Configuration config = new Configuration(c.getPid());
-
-            final Enumeration<String> keyEnum = c.getProperties().keys();
-            while ( keyEnum.hasMoreElements() ) {
-                final String key = keyEnum.nextElement();
-                final Object val = c.getProperties().get(key);
-                config.getProperties().put(key, val);
-            }
-            if ( config.getProperties().get(Configuration.PROP_ARTIFACT_ID) != null ) {
-                throw new IOException(exceptionPrefix + "Configuration must not define property " + Configuration.PROP_ARTIFACT_ID);
-            }
-            if ( artifact != null ) {
-                config.getProperties().put(Configuration.PROP_ARTIFACT_ID, artifact.getId().toMvnId());
-            }
-            for(final Configuration current : container) {
-                if ( current.equals(config) ) {
-                    throw new IOException(exceptionPrefix + "Duplicate configuration " + config);
-                }
-            }
-            container.add(config);
-        }
-    }
-
-
-    protected void readConfigurations(final Map<String, Object> map,
-            final Configurations container) throws IOException {
-        if ( map.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
-            checkType(JSONConstants.FEATURE_CONFIGURATIONS, map.get(JSONConstants.FEATURE_CONFIGURATIONS), Map.class);
-            addConfigurations(map, null, container);
-        }
-    }
-
-    protected void readFrameworkProperties(final Map<String, Object> map,
-            final Map<String,String> container) throws IOException {
-        if ( map.containsKey(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES) ) {
-            final Object propsObj= map.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES);
-            checkType(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES, propsObj, Map.class);
-
-            @SuppressWarnings("unchecked")
-            final Map<String, Object> props = (Map<String, Object>) propsObj;
-            for(final Map.Entry<String, Object> entry : props.entrySet()) {
-                checkType("framework property value", entry.getValue(), String.class, Boolean.class, Number.class);
-                if ( container.get(entry.getKey()) != null ) {
-                    throw new IOException(this.exceptionPrefix + "Duplicate framework property " + entry.getKey());
-                }
-                container.put(entry.getKey(), entry.getValue().toString());
-            }
-
-        }
-    }
-
-    protected void readExtensions(final Map<String, Object> map,
-            final List<String> keywords,
-            final Extensions container,
-            final Configurations configContainer) throws IOException {
-        final Set<String> keySet = new HashSet<>(map.keySet());
-        keySet.removeAll(keywords);
-        // the remaining keys are considered extensions!
-        for(final String key : keySet) {
-            final int pos = key.indexOf(':');
-            final String postfix = pos == -1 ? null : key.substring(pos + 1);
-            final int sep = (postfix == null ? key.indexOf('|') : postfix.indexOf('|'));
-            final String name;
-            final String type;
-            final String state;
-            if ( pos == -1 ) {
-                type = ExtensionType.ARTIFACTS.name();
-                if ( sep == -1 ) {
-                    name = key;
-                    state = ExtensionState.OPTIONAL.name();
-                } else {
-                    name = key.substring(0, sep);
-                    state = key.substring(sep + 1);
-                }
-            } else {
-                name = key.substring(0, pos);
-                if ( sep == -1 ) {
-                    type = postfix;
-                    state = ExtensionState.OPTIONAL.name();
-                } else {
-                    type = postfix.substring(0, sep);
-                    state = postfix.substring(sep + 1);
-                }
-            }
-            if ( JSONConstants.FEATURE_KNOWN_PROPERTIES.contains(name) ) {
-                throw new IOException(this.exceptionPrefix + "Extension is using reserved name : " + name);
-            }
-            if ( container.getByName(name) != null ) {
-                throw new IOException(exceptionPrefix + "Duplicate extension with name " + name);
-            }
-
-            final ExtensionType extType = ExtensionType.valueOf(type);
-            final ExtensionState extState;
-            if (ExtensionState.OPTIONAL.name().equalsIgnoreCase(state)) {
-                extState = ExtensionState.OPTIONAL;
-            } else if (ExtensionState.REQUIRED.name().equalsIgnoreCase(state)) {
-                extState = ExtensionState.REQUIRED;
-            } else if (ExtensionState.TRANSIENT.name().equalsIgnoreCase(state)) {
-                extState = ExtensionState.TRANSIENT;
-            } else {
-                final boolean opt = Boolean.valueOf(state).booleanValue();
-                extState = opt ? ExtensionState.REQUIRED : ExtensionState.OPTIONAL;
-            }
-
-            final Extension ext = new Extension(extType, name, extState);
-            final Object value = map.get(key);
-            switch ( extType ) {
-                case ARTIFACTS : final List<Artifact> list = new ArrayList<>();
-                                 readArtifacts("Extension " + name, "artifact", list, value, configContainer);
-                                 for(final Artifact a : list) {
-                                     if ( ext.getArtifacts().contains(a) ) {
-                                         throw new IOException(exceptionPrefix + "Duplicate artifact in extension " + name + " : " + a.getId().toMvnId());
-                                     }
-                                     ext.getArtifacts().add(a);
-                                 }
-                                 break;
-                case JSON : checkType("JSON Extension " + name, value, Map.class, List.class);
-                            final JsonStructure struct = build(value);
-                            ext.setJSONStructure(struct);
-                            break;
-                case TEXT : checkType("Text Extension " + name, value, String.class, List.class);
-                            if ( value instanceof String ) {
-                                // string
-                                ext.setText(value.toString());
-                            } else {
-                                // list (array of strings)
-                                @SuppressWarnings("unchecked")
-                                final List<Object> l = (List<Object>)value;
-                                final StringBuilder sb = new StringBuilder();
-                                for(final Object o : l) {
-                                    checkType("Text Extension " + name + ", value " + o, o, String.class);
-                                    sb.append(o.toString());
-                                    sb.append('\n');
-                                }
-                                ext.setText(sb.toString());
-                            }
-                            break;
-            }
-
-            container.add(ext);
-        }
-    }
-
-    private JsonStructure build(final Object value) {
-        if ( value instanceof List ) {
-            @SuppressWarnings("unchecked")
-            final List<Object> list = (List<Object>)value;
-            final JsonArrayBuilder builder = Json.createArrayBuilder();
-            for(final Object obj : list) {
-                if ( obj instanceof String ) {
-                    builder.add(obj.toString());
-                } else if ( obj instanceof Long ) {
-                    builder.add((Long)obj);
-                } else if ( obj instanceof Double ) {
-                    builder.add((Double)obj);
-                } else if (obj instanceof Boolean ) {
-                    builder.add((Boolean)obj);
-                } else if ( obj instanceof Map ) {
-                    builder.add(build(obj));
-                } else if ( obj instanceof List ) {
-                    builder.add(build(obj));
-                }
-
-            }
-            return builder.build();
-        } else if ( value instanceof Map ) {
-            @SuppressWarnings("unchecked")
-            final Map<String, Object> map = (Map<String, Object>)value;
-            final JsonObjectBuilder builder = Json.createObjectBuilder();
-            for(final Map.Entry<String, Object> entry : map.entrySet()) {
-                if ( entry.getValue() instanceof String ) {
-                    builder.add(entry.getKey(), entry.getValue().toString());
-                } else if ( entry.getValue() instanceof Long ) {
-                    builder.add(entry.getKey(), (Long)entry.getValue());
-                } else if ( entry.getValue() instanceof Double ) {
-                    builder.add(entry.getKey(), (Double)entry.getValue());
-                } else if ( entry.getValue() instanceof Boolean ) {
-                    builder.add(entry.getKey(), (Boolean)entry.getValue());
-                } else if ( entry.getValue() instanceof Map ) {
-                    builder.add(entry.getKey(), build(entry.getValue()));
-                } else if ( entry.getValue() instanceof List ) {
-                    builder.add(entry.getKey(), build(entry.getValue()));
-                }
-            }
-            return builder.build();
-        }
-        return null;
-    }
-
-    /**
-     * Check if the value is one of the provided types
-     * @param key A key for the error message
-     * @param val The value to check
-     * @param types The allowed types, can also include {@code null} if null is an allowed value.
-     * @throws IOException If the val is not of the specified types
-     */
-    protected void checkType(final String key, final Object val, Class<?>...types) throws IOException {
-        boolean valid = false;
-        for(final Class<?> c : types) {
-            if (c == null) {
-                if ( val == null) {
-                    valid = true;
-                    break;
-                }
-            } else if ( c.isInstance(val) ) {
-                valid = true;
-                break;
-            }
-        }
-        if ( !valid ) {
-            throw new IOException(this.exceptionPrefix + "Key " + key + " is not one of the allowed types " + Arrays.toString(types) + " : " + val.getClass());
-        }
-    }
-
-    protected Prototype readPrototype(final Map<String, Object> map) throws IOException {
-        if ( map.containsKey(JSONConstants.FEATURE_PROTOTYPE)) {
-            final Object prototypeObj = map.get(JSONConstants.FEATURE_PROTOTYPE);
-            checkType(JSONConstants.FEATURE_PROTOTYPE, prototypeObj, Map.class, String.class);
-
-            final Prototype prototype;
-            if ( prototypeObj instanceof String ) {
-                final ArtifactId id = ArtifactId.parse(prototypeObj.toString());
-                prototype = new Prototype(id);
-            } else {
-                @SuppressWarnings("unchecked")
-                final Map<String, Object> obj = (Map<String, Object>) prototypeObj;
-                if ( !obj.containsKey(JSONConstants.ARTIFACT_ID) ) {
-                    throw new IOException(exceptionPrefix + " prototype is missing required artifact id");
-                }
-                checkType("Prototype " + JSONConstants.ARTIFACT_ID, obj.get(JSONConstants.ARTIFACT_ID), String.class);
-                final ArtifactId id = ArtifactId.parse(obj.get(JSONConstants.ARTIFACT_ID).toString());
-                prototype = new Prototype(id);
-
-                if ( obj.containsKey(JSONConstants.PROTOTYPE_REMOVALS) ) {
-                    checkType("Prototype removals", obj.get(JSONConstants.PROTOTYPE_REMOVALS), Map.class);
-                    @SuppressWarnings("unchecked")
-                    final Map<String, Object> removalObj = (Map<String, Object>) obj.get(JSONConstants.PROTOTYPE_REMOVALS);
-                    if ( removalObj.containsKey(JSONConstants.FEATURE_BUNDLES) ) {
-                        checkType("Prototype removal bundles", removalObj.get(JSONConstants.FEATURE_BUNDLES), List.class);
-                        @SuppressWarnings("unchecked")
-                        final List<Object> list = (List<Object>)removalObj.get(JSONConstants.FEATURE_BUNDLES);
-                        for(final Object val : list) {
-                            checkType("Prototype removal bundles", val, String.class);
-                            if ( val.toString().startsWith("#")) {
-                                continue;
-                            }
-                            prototype.getBundleRemovals().add(ArtifactId.parse(val.toString()));
-                        }
-                    }
-                    if ( removalObj.containsKey(JSONConstants.FEATURE_CONFIGURATIONS) ) {
-                        checkType("Prototype removal configuration", removalObj.get(JSONConstants.FEATURE_CONFIGURATIONS), List.class);
-                        @SuppressWarnings("unchecked")
-                        final List<Object> list = (List<Object>)removalObj.get(JSONConstants.FEATURE_CONFIGURATIONS);
-                        for(final Object val : list) {
-                            checkType("Prototype removal configuration", val, String.class);
-                            prototype.getConfigurationRemovals().add(val.toString());
-                        }
-                    }
-                    if ( removalObj.containsKey(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES) ) {
-                        checkType("Prototype removal framework properties", removalObj.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES), List.class);
-                        @SuppressWarnings("unchecked")
-                        final List<Object> list = (List<Object>)removalObj.get(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES);
-                        for(final Object val : list) {
-                            checkType("Prototype removal framework properties", val, String.class);
-                            prototype.getFrameworkPropertiesRemovals().add(val.toString());
-                        }
-                    }
-                    if ( removalObj.containsKey(JSONConstants.PROTOTYPE_EXTENSION_REMOVALS) ) {
-                        checkType("Prototype removal extensions", removalObj.get(JSONConstants.PROTOTYPE_EXTENSION_REMOVALS), List.class);
-                        @SuppressWarnings("unchecked")
-                        final List<Object> list = (List<Object>)removalObj.get(JSONConstants.PROTOTYPE_EXTENSION_REMOVALS);
-                        for(final Object val : list) {
-                            checkType("Prototype removal extension", val, String.class, Map.class);
-                            if ( val instanceof String ) {
-                                if ( val.toString().startsWith("#")) {
-                                    continue;
-                                }
-                                prototype.getExtensionRemovals().add(val.toString());
-                            } else {
-                                @SuppressWarnings("unchecked")
-                                final Map<String, Object> removalMap = (Map<String, Object>)val;
-                                final Object nameObj = removalMap.get("name");
-                                checkType("Prototype removal extension", nameObj, String.class);
-                                if ( removalMap.containsKey("artifacts") ) {
-                                    checkType("Prototype removal extension artifacts", removalMap.get("artifacts"), List.class);
-                                    @SuppressWarnings("unchecked")
-                                    final List<Object> artifactList = (List<Object>)removalMap.get("artifacts");
-                                    final List<ArtifactId> ids = new ArrayList<>();
-                                    for(final Object aid : artifactList) {
-                                        checkType("Prototype removal extension artifact", aid, String.class);
-                                        ids.add(ArtifactId.parse(aid.toString()));
-                                    }
-                                    prototype.getArtifactExtensionRemovals().put(nameObj.toString(), ids);
-                                } else {
-                                    prototype.getExtensionRemovals().add(nameObj.toString());
-                                }
-                            }
-                        }
-                    }
-                    readRequirements(removalObj, prototype.getRequirementRemovals());
-                    readCapabilities(removalObj, prototype.getCapabilityRemovals());
-
-                }
-            }
-            return prototype;
-        }
-        return null;
-    }
-
-    protected void readRequirements(Map<String, Object> map, final List<MatchingRequirement> container)
-            throws IOException {
-        if ( map.containsKey(JSONConstants.FEATURE_REQUIREMENTS)) {
-            final Object reqObj = map.get(JSONConstants.FEATURE_REQUIREMENTS);
-            checkType(JSONConstants.FEATURE_REQUIREMENTS, reqObj, List.class);
-
-            @SuppressWarnings("unchecked")
-            final List<Object> requirements = (List<Object>)reqObj;
-            for(final Object req : requirements) {
-                checkType("Requirement", req, Map.class);
-                @SuppressWarnings("unchecked")
-                final Map<String, Object> obj = (Map<String, Object>) req;
-
-                if ( !obj.containsKey(JSONConstants.REQCAP_NAMESPACE) ) {
-                    throw new IOException(this.exceptionPrefix + "Namespace is missing for requirement");
-                }
-                checkType("Requirement namespace", obj.get(JSONConstants.REQCAP_NAMESPACE), String.class);
-
-                Map<String, Object> attrMap = new HashMap<>();
-                if ( obj.containsKey(JSONConstants.REQCAP_ATTRIBUTES) ) {
-                    checkType("Requirement attributes", obj.get(JSONConstants.REQCAP_ATTRIBUTES), Map.class);
-                    @SuppressWarnings("unchecked")
-                    final Map<String, Object> attrs = (Map<String, Object>)obj.get(JSONConstants.REQCAP_ATTRIBUTES);
-                    attrs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalAttribute(key, value, attrMap::put)));
-                }
-
-                Map<String, String> dirMap = new HashMap<>();
-                if ( obj.containsKey(JSONConstants.REQCAP_DIRECTIVES) ) {
-                    checkType("Requirement directives", obj.get(JSONConstants.REQCAP_DIRECTIVES), Map.class);
-                    @SuppressWarnings("unchecked")
-                    final Map<String, Object> dirs = (Map<String, Object>)obj.get(JSONConstants.REQCAP_DIRECTIVES);
-                    dirs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalDirective(key, value, dirMap::put)));
-                }
-
-                final MatchingRequirement r = new MatchingRequirementImpl(null,
-                        obj.get(JSONConstants.REQCAP_NAMESPACE).toString(), dirMap, attrMap);
-                container.add(r);
-            }
-        }
-    }
-
-    protected void readCapabilities(Map<String, Object> map, final List<Capability> container) throws IOException {
-        if ( map.containsKey(JSONConstants.FEATURE_CAPABILITIES)) {
-            final Object capObj = map.get(JSONConstants.FEATURE_CAPABILITIES);
-            checkType(JSONConstants.FEATURE_CAPABILITIES, capObj, List.class);
-
-            @SuppressWarnings("unchecked")
-            final List<Object> capabilities = (List<Object>)capObj;
-            for(final Object cap : capabilities) {
-                checkType("Capability", cap, Map.class);
-                @SuppressWarnings("unchecked")
-                final Map<String, Object> obj = (Map<String, Object>) cap;
-
-                if ( !obj.containsKey(JSONConstants.REQCAP_NAMESPACE) ) {
-                    throw new IOException(this.exceptionPrefix + "Namespace is missing for capability");
-                }
-                checkType("Capability namespace", obj.get(JSONConstants.REQCAP_NAMESPACE), String.class);
-
-                Map<String, Object> attrMap = new HashMap<>();
-                if ( obj.containsKey(JSONConstants.REQCAP_ATTRIBUTES) ) {
-                    checkType("Capability attributes", obj.get(JSONConstants.REQCAP_ATTRIBUTES), Map.class);
-                    @SuppressWarnings("unchecked")
-                    final Map<String, Object> attrs = (Map<String, Object>)obj.get(JSONConstants.REQCAP_ATTRIBUTES);
-                    attrs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalAttribute(key, value, attrMap::put)));
-                }
-
-                Map<String, String> dirMap = new HashMap<>();
-                if ( obj.containsKey(JSONConstants.REQCAP_DIRECTIVES) ) {
-                    checkType("Capability directives", obj.get(JSONConstants.REQCAP_DIRECTIVES), Map.class);
-                    @SuppressWarnings("unchecked")
-                    final Map<String, Object> dirs = (Map<String, Object>) obj.get(JSONConstants.REQCAP_DIRECTIVES);
-                    dirs.forEach(rethrowBiConsumer((key, value) -> ManifestUtils.unmarshalDirective(key, value, dirMap::put)));
-                }
-
-                final Capability c = new CapabilityImpl(null, obj.get(JSONConstants.REQCAP_NAMESPACE).toString(), dirMap, attrMap);
-                container.add(c);
-            }
-        }
-    }
-
-    @FunctionalInterface
-    private interface BiConsumer_WithExceptions<T, V, E extends Exception> {
-        void accept(T t, V u) throws E;
-    }
-
-    private static <T, V, E extends Exception> BiConsumer<T, V> rethrowBiConsumer(BiConsumer_WithExceptions<T, V, E> biConsumer) {
-        return (t, u) -> {
-            try {
-                biConsumer.accept(t, u);
-            } catch (Exception exception) {
-                throwAsUnchecked(exception);
-            }
-        };
-    }
-
-    @SuppressWarnings ("unchecked")
-    private static <E extends Throwable> void throwAsUnchecked(Exception exception) throws E {
-        throw (E) exception;
-    }
-
-    private static class MatchingRequirementImpl extends RequirementImpl implements MatchingRequirement {
-
-        public MatchingRequirementImpl(Resource res, String ns, Map<String, String> dirs, Map<String, Object> attrs) {
-            super(res, ns, dirs, attrs);
-        }
-    }
-}
diff --git a/src/main/java/org/apache/sling/feature/io/json/JSONWriterBase.java b/src/main/java/org/apache/sling/feature/io/json/JSONWriterBase.java
deleted file mode 100644
index 9d17744..0000000
--- a/src/main/java/org/apache/sling/feature/io/json/JSONWriterBase.java
+++ /dev/null
@@ -1,311 +0,0 @@
-/*
- * 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.io.json;
-
-import java.io.Writer;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import javax.json.Json;
-import javax.json.stream.JsonGenerator;
-import javax.json.stream.JsonGeneratorFactory;
-
-import org.apache.sling.feature.Artifact;
-import org.apache.sling.feature.ArtifactId;
-import org.apache.sling.feature.Bundles;
-import org.apache.sling.feature.Configuration;
-import org.apache.sling.feature.Configurations;
-import org.apache.sling.feature.Extension;
-import org.apache.sling.feature.ExtensionType;
-import org.apache.sling.feature.MatchingRequirement;
-import org.apache.sling.feature.Prototype;
-import org.apache.sling.feature.io.CloseShieldWriter;
-import org.apache.sling.feature.io.ConfiguratorUtil;
-import org.osgi.resource.Capability;
-import org.osgi.resource.Requirement;
-
-/**
- * Common functionality for writing JSON
- */
-abstract class JSONWriterBase {
-
-    private final JsonGeneratorFactory generatorFactory = Json.createGeneratorFactory(Collections.singletonMap(JsonGenerator.PRETTY_PRINTING, true));
-
-    protected final JsonGenerator newGenerator(final Writer writer) {
-        // prevent closing of the underlying writer
-        Writer closeShieldWriter = new CloseShieldWriter(writer);
-        return generatorFactory.createGenerator(closeShieldWriter);
-    }
-
-    protected void writeBundles(final JsonGenerator generator,
-            final Bundles bundles,
-            final Configurations allConfigs) {
-        // bundles
-        if ( !bundles.isEmpty() ) {
-            generator.writeStartArray(JSONConstants.FEATURE_BUNDLES);
-
-            for(final Artifact artifact : bundles) {
-                final Configurations cfgs = new Configurations();
-                for(final Configuration cfg : allConfigs) {
-                    final String artifactProp = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT_ID);
-                    if ( artifact.getId().toMvnId().equals(artifactProp) ) {
-                        cfgs.add(cfg);
-                    }
-                }
-                Map<String,String> md = artifact.getMetadata();
-                if ( md.isEmpty() && cfgs.isEmpty() ) {
-                    generator.write(artifact.getId().toMvnId());
-                } else {
-                    generator.writeStartObject();
-                    generator.write(JSONConstants.ARTIFACT_ID, artifact.getId().toMvnId());
-
-                    Object runmodes = md.remove("runmodes");
-                    if (runmodes instanceof String) {
-                        md.put("run-modes", (String) runmodes);
-                    }
-
-                    for(final Map.Entry<String, String> me : md.entrySet()) {
-                        generator.write(me.getKey(), me.getValue());
-                    }
-
-                    generator.writeEnd();
-                }
-            }
-
-            generator.writeEnd();
-        }
-    }
-
-    /**
-     * Write the list of configurations into a "configurations" element
-     * @param generator The json generator
-     * @param cfgs The list of configurations
-     */
-    protected void writeConfigurations(final JsonGenerator generator, final Configurations cfgs) {
-        if ( cfgs.isEmpty() ) {
-            return;
-        }
-
-        generator.writeStartObject(JSONConstants.FEATURE_CONFIGURATIONS);
-
-        for(final Configuration cfg : cfgs) {
-            generator.writeStartObject(cfg.getPid());
-            ConfiguratorUtil.writeConfiguration(generator, cfg.getConfigurationProperties());
-            generator.writeEnd();
-        }
-
-        generator.writeEnd();
-    }
-
-    protected void writeVariables(final JsonGenerator generator, final Map<String,String> vars) {
-        if ( !vars.isEmpty()) {
-            generator.writeStartObject(JSONConstants.FEATURE_VARIABLES);
-
-            for (final Map.Entry<String, String> entry : vars.entrySet()) {
-                String val = entry.getValue();
-                if (val != null)
-                    generator.write(entry.getKey(), val);
-                else
-                    generator.writeNull(entry.getKey());
-            }
-
-            generator.writeEnd();
-        }
-    }
-
-    protected void writeFrameworkProperties(final JsonGenerator generator, final Map<String,String> props) {
-        // framework properties
-        if ( !props.isEmpty() ) {
-            generator.writeStartObject(JSONConstants.FEATURE_FRAMEWORK_PROPERTIES);
-            for(final Map.Entry<String, String> entry : props.entrySet()) {
-                generator.write(entry.getKey(), entry.getValue());
-            }
-            generator.writeEnd();
-        }
-    }
-
-    protected void writeExtensions(final JsonGenerator generator,
-            final List<Extension> extensions,
-            final Configurations allConfigs) {
-        for(final Extension ext : extensions) {
-            final String state;
-            switch (ext.getState()) {
-            case OPTIONAL:
-                state = "false";
-                break;
-            case REQUIRED:
-                state = "true";
-                break;
-            default:
-                state = ext.getState().name();
-            }
-            final String key = ext.getName().concat(":").concat(ext.getType().name()).concat("|").concat(state);
-            if ( ext.getType() == ExtensionType.JSON ) {
-                generator.write(key, ext.getJSONStructure());
-            } else if ( ext.getType() == ExtensionType.TEXT ) {
-                generator.writeStartArray(key);
-                for(String line : ext.getText().split("\n")) {
-                    generator.write(line);
-                }
-                generator.writeEnd();
-            } else {
-                generator.writeStartArray(key);
-                for(final Artifact artifact : ext.getArtifacts()) {
-                    final Configurations artifactCfgs = new Configurations();
-                    for(final Configuration cfg : allConfigs) {
-                        final String artifactProp = (String)cfg.getProperties().get(Configuration.PROP_ARTIFACT_ID);
-                        if (  artifact.getId().toMvnId().equals(artifactProp) ) {
-                            artifactCfgs.add(cfg);
-                        }
-                    }
-                    if ( artifact.getMetadata().isEmpty() && artifactCfgs.isEmpty() ) {
-                        generator.write(artifact.getId().toMvnId());
-                    } else {
-                        generator.writeStartObject();
-                        generator.write(JSONConstants.ARTIFACT_ID, artifact.getId().toMvnId());
-
-                        for(final Map.Entry<String, String> me : artifact.getMetadata().entrySet()) {
-                            generator.write(me.getKey(), me.getValue());
-                        }
-
-                        writeConfigurations(generator, artifactCfgs);
-
-                        generator.writeEnd();
-                    }
-                }
-                generator.writeEnd();
-            }
-        }
-    }
-
-    protected void writeProperty(final JsonGenerator generator, final String key, final String value) {
-        if ( value != null ) {
-            generator.write(key, value);
-        }
-    }
-
-    protected <T> void writeList(final JsonGenerator generator, final String name, final Collection<T> values) {
-        if (!values.isEmpty()) {
-            generator.writeStartArray(name);
-            for (T value : values) {
-                generator.write(value.toString());
-            }
-            generator.writeEnd();
-        }
-    }
-
-    protected void writePrototype(final JsonGenerator generator, final Prototype inc) {
-        if (inc == null) {
-            return;
-        }
-
-        if ( inc.getArtifactExtensionRemovals().isEmpty()
-             && inc.getBundleRemovals().isEmpty()
-             && inc.getConfigurationRemovals().isEmpty()
-             && inc.getFrameworkPropertiesRemovals().isEmpty()
-             && inc.getRequirementRemovals().isEmpty()
-             && inc.getCapabilityRemovals().isEmpty() ) {
-
-            generator.write(JSONConstants.FEATURE_PROTOTYPE, inc.getId().toMvnId());
-        } else {
-            generator.writeStartObject(JSONConstants.FEATURE_PROTOTYPE);
-            writeProperty(generator, JSONConstants.ARTIFACT_ID, inc.getId().toMvnId());
-
-            generator.writeStartObject(JSONConstants.PROTOTYPE_REMOVALS);
-
-            if ( !inc.getArtifactExtensionRemovals().isEmpty()
-                 || inc.getExtensionRemovals().isEmpty() ) {
-                generator.writeStartArray(JSONConstants.PROTOTYPE_EXTENSION_REMOVALS);
-
-                for(final String id : inc.getExtensionRemovals()) {
-                    generator.write(id);
-                }
-                for(final Map.Entry<String, List<ArtifactId>> entry : inc.getArtifactExtensionRemovals().entrySet()) {
-                    generator.writeStartObject();
-
-                    writeList(generator, entry.getKey(), entry.getValue());
-
-                    generator.writeEnd();
-                }
-
-                generator.writeEnd();
-            }
-            writeList(generator, JSONConstants.FEATURE_CONFIGURATIONS, inc.getConfigurationRemovals());
-            writeList(generator, JSONConstants.FEATURE_BUNDLES, inc.getBundleRemovals());
-            writeList(generator, JSONConstants.FEATURE_FRAMEWORK_PROPERTIES, inc.getFrameworkPropertiesRemovals());
-
-            writeRequirements(generator, inc.getRequirementRemovals());
-            writeCapabilities(generator, inc.getCapabilityRemovals());
-
-            generator.writeEnd().writeEnd();
-        }
-    }
-
-    protected void writeRequirements(final JsonGenerator generator, final List<MatchingRequirement> requirements) {
-        if (requirements.isEmpty()) {
-            return;
-        }
-
-        generator.writeStartArray(JSONConstants.FEATURE_REQUIREMENTS);
-
-        for(final Requirement req : requirements) {
-            generator.writeStartObject();
-            writeProperty(generator, JSONConstants.REQCAP_NAMESPACE, req.getNamespace());
-            if ( !req.getAttributes().isEmpty() ) {
-                generator.writeStartObject(JSONConstants.REQCAP_ATTRIBUTES);
-                req.getAttributes().forEach((key, value) -> ManifestUtils.marshalAttribute(key, value, generator::write));
-                generator.writeEnd();
-            }
-            if ( !req.getDirectives().isEmpty() ) {
-                generator.writeStartObject(JSONConstants.REQCAP_DIRECTIVES);
-                req.getDirectives().forEach((key, value) -> ManifestUtils.marshalDirective(key, value, generator::write));
-                generator.writeEnd();
-            }
-            generator.writeEnd();
-        }
-
-        generator.writeEnd();
-    }
-
-    protected void writeCapabilities(final JsonGenerator generator, final List<Capability> capabilities) {
-        if (capabilities.isEmpty()) {
-            return;
-        }
-
-        generator.writeStartArray(JSONConstants.FEATURE_CAPABILITIES);
-
-        for(final Capability cap : capabilities) {
-            generator.writeStartObject();
-            writeProperty(generator, JSONConstants.REQCAP_NAMESPACE, cap.getNamespace());
-            if ( !cap.getAttributes().isEmpty() ) {
-                generator.writeStartObject(JSONConstants.REQCAP_ATTRIBUTES);
-                cap.getAttributes().forEach((key, value) -> ManifestUtils.marshalAttribute(key, value, generator::write));
-                generator.writeEnd();
-            }
-            if ( !cap.getDirectives().isEmpty() ) {
-                generator.writeStartObject(JSONConstants.REQCAP_DIRECTIVES);
-                cap.getDirectives().forEach((key, value) -> ManifestUtils.marshalDirective(key, value, generator::write));
-                generator.writeEnd();
-            }
-            generator.writeEnd();
-        }
-
-        generator.writeEnd();
-    }
-}
diff --git a/src/main/java/org/apache/sling/feature/io/json/ManifestUtils.java b/src/main/java/org/apache/sling/feature/io/json/ManifestUtils.java
index ee6585f..dae75c8 100644
--- a/src/main/java/org/apache/sling/feature/io/json/ManifestUtils.java
+++ b/src/main/java/org/apache/sling/feature/io/json/ManifestUtils.java
@@ -16,11 +16,6 @@
  */
 package org.apache.sling.feature.io.json;
 
-import org.apache.felix.utils.resource.CapabilityImpl;
-import org.osgi.framework.BundleException;
-import org.osgi.framework.Version;
-import org.osgi.resource.Capability;
-
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -31,21 +26,29 @@ import java.util.function.BiConsumer;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
+import javax.json.JsonValue;
+
+import org.apache.felix.cm.json.Configurations;
+import org.apache.felix.utils.resource.CapabilityImpl;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Version;
+import org.osgi.resource.Capability;
+
 // This class can be picked up from Felix Utils once it has been moved there. At that point
 // this class can be removed.
 class ManifestUtils {
-    public static void unmarshalAttribute(String key, Object value, BiConsumer<String, Object> sink) throws IOException {
-        unmarshal(key + "=" + value, Capability::getAttributes, sink);
+    public static void unmarshalAttribute(String key, JsonValue value, BiConsumer<String, Object> sink) throws IOException {
+        unmarshal(key.concat("=").concat(Configurations.convertToObject(value).toString()), Capability::getAttributes, sink);
     }
 
-    public static void unmarshalDirective(String key, Object value, BiConsumer<String, String> sink) throws IOException {
-        unmarshal(key + ":=" + value, Capability::getDirectives, sink);
+    public static void unmarshalDirective(String key, JsonValue value, BiConsumer<String, String> sink) throws IOException {
+        unmarshal(key.concat(":=").concat(Configurations.convertToObject(value).toString()), Capability::getDirectives, sink);
     }
 
     private static <T> void unmarshal(String header, Function<Capability, Map<String, T>> lookup, BiConsumer<String, T> sink) throws IOException {
         try {
             convertProvideCapabilities(
-                    normalizeCapabilityClauses(parseStandardHeader("foo;" + header), "2"))
+                    normalizeCapabilityClauses(parseStandardHeader("foo;".concat(header)), "2"))
                     .forEach(capability -> lookup.apply(capability).forEach(sink));
         } catch (Exception e) {
             throw new IOException(e);
diff --git a/src/test/java/org/apache/sling/feature/io/ConfiguratorUtilTest.java b/src/test/java/org/apache/sling/feature/io/ConfiguratorUtilTest.java
index 46b6ba8..4ad71eb 100644
--- a/src/test/java/org/apache/sling/feature/io/ConfiguratorUtilTest.java
+++ b/src/test/java/org/apache/sling/feature/io/ConfiguratorUtilTest.java
@@ -17,20 +17,15 @@
 package org.apache.sling.feature.io;
 
 import java.io.IOException;
+import java.io.StringReader;
 import java.io.StringWriter;
-import java.net.MalformedURLException;
-import java.net.URL;
 import java.util.Arrays;
 import java.util.Dictionary;
 import java.util.Hashtable;
 import java.util.Map;
 import java.util.Objects;
 
-import org.apache.commons.lang3.StringUtils;
-import org.apache.felix.configurator.impl.json.JSONUtil;
-import org.apache.felix.configurator.impl.json.TypeConverter;
-import org.apache.felix.configurator.impl.model.ConfigurationFile;
-import org.apache.sling.feature.io.ConfiguratorUtil;
+import org.apache.felix.cm.json.Configurations;
 import org.hamcrest.Description;
 import org.hamcrest.TypeSafeDiagnosingMatcher;
 import org.hamcrest.core.Every;
@@ -86,22 +81,12 @@ public class ConfiguratorUtilTest {
         assertConfigurationJson(writer.toString(), props);
     }
 
-    protected void assertConfigurationJson(String json, Dictionary<String, Object> expectedProps) throws MalformedURLException {
-        final JSONUtil.Report report = new JSONUtil.Report();
-        StringBuilder sb = new StringBuilder("{ \"");
-        sb.append("myid");
-        sb.append("\" : ");
-        sb.append(json);
-        sb.append("}");
-        final ConfigurationFile configurationFile = JSONUtil.readJSON(new TypeConverter(null),"name", new URL("file:///configtest"),
-                0, sb.toString(), report);
-        if ( !report.errors.isEmpty() || !report.warnings.isEmpty() ) {
-            Assert.fail("JSON is not the right format: \nErrors: " + StringUtils.join(report.errors) + "\nWarnings: " + StringUtils.join(report.warnings));
-        }
+    protected void assertConfigurationJson(String json, Dictionary<String, Object> expectedProps) throws IOException {
+        final Hashtable<String, Object> readProps = Configurations.buildReader().verifyAsBundleResource(true).build(new StringReader(json)).readConfiguration();
         // convert to maps for easier comparison
         Converter converter = Converters.standardConverter();
         Map<String, Object> expectedPropsMap = converter.convert(expectedProps).to(new TypeReference<Map<String,Object>>(){});
-        Map<String, Object> actualPropsMap = converter.convert(configurationFile.getConfigurations().get(0).getProperties()).to(new TypeReference<Map<String,Object>>(){});
+        Map<String, Object> actualPropsMap = converter.convert(readProps).to(new TypeReference<Map<String,Object>>(){});
         Assert.assertThat(actualPropsMap.entrySet(), Every.everyItem(new MapEntryMatcher<>(expectedPropsMap)));
     }
 
@@ -127,7 +112,7 @@ public class ConfiguratorUtilTest {
                 boolean isEqual;
                 if (item.getValue().getClass().isArray()) {
                     isEqual = Objects.deepEquals(expectedMap.get(item.getKey()), item.getValue());
-                    
+
                 } else {
                     isEqual = expectedMap.get(item.getKey()).equals(item.getValue());
                 }
diff --git a/src/test/java/org/apache/sling/feature/io/IOUtilsTest.java b/src/test/java/org/apache/sling/feature/io/IOUtilsTest.java
index 4c72e0f..08920a4 100644
--- a/src/test/java/org/apache/sling/feature/io/IOUtilsTest.java
+++ b/src/test/java/org/apache/sling/feature/io/IOUtilsTest.java
@@ -66,97 +66,105 @@ public class IOUtilsTest {
     }
 
     @Test public void testGetFileFromURL() throws IOException {
-        File file = File.createTempFile("IOUtilsTest \\\\+%23öäü^^^°$::", ".test");
+        File file = File.createTempFile("IOUtilsTest", ".test");
 
-        try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
-            writer.println("Hello");
-        }
-
-        assertEquals(file, IOUtils.getFileFromURL(new URL("file:" + file.getPath()), false, null));
-
-        assertEquals(file, IOUtils.getFileFromURL(file.toURI().toURL(), false, null));
-
-        URL url = new URL(null,"bla:" + file.toURI().toURL(), new URLStreamHandler() {
-            @Override
-            protected URLConnection openConnection(URL u){
-                return new URLConnection(u) {
-                    @Override
-                    public void connect()
-                    {
-
-                    }
-
-                    @Override
-                    public InputStream getInputStream() throws IOException
-                    {
-                        return new FileInputStream(file);
-                    }
-                };
+        try {
+            try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) {
+                writer.println("Hello");
             }
-        });
-
-        assertNull(IOUtils.getFileFromURL(url, false, null));
-        File tmp = IOUtils.getFileFromURL(url, true, null);
 
-        assertNotEquals(file, tmp);
-        try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(tmp), "UTF-8"))) {
-            assertEquals("Hello", reader.readLine());
+            assertEquals(file, IOUtils.getFileFromURL(file.toURI().toURL(), false, null));
+
+            URL url = new URL(null,"bla:" + file.toURI().toURL(), new URLStreamHandler() {
+                @Override
+                protected URLConnection openConnection(URL u){
+                    return new URLConnection(u) {
+                        @Override
+                        public void connect()
+                        {
+
+                        }
+
+                        @Override
+                        public InputStream getInputStream() throws IOException
+                        {
+                            return new FileInputStream(file);
+                        }
+                    };
+                }
+            });
+
+            assertNull(IOUtils.getFileFromURL(url, false, null));
+            File tmp = IOUtils.getFileFromURL(url, true, null);
+
+            assertNotEquals(file, tmp);
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(tmp), "UTF-8"))) {
+                assertEquals("Hello", reader.readLine());
+            }
+        } finally {
+            file.delete();
         }
+        File jarFile = File.createTempFile("IOUtilsTes", ".jar");
+        try {
 
-        File jarFile = File.createTempFile("\"IOUtilsTest \\\\\\\\+%23öäü^^^°$::\"", ".jar");
+            try (JarOutputStream output = new JarOutputStream(new FileOutputStream(jarFile))) {
+                output.putNextEntry(new JarEntry("test"));
+                output.write("Hello".getBytes());
+                output.closeEntry();
+            }
 
-        try (JarOutputStream output = new JarOutputStream(new FileOutputStream(jarFile))) {
-            output.putNextEntry(new JarEntry("test"));
-            output.write("Hello".getBytes());
-            output.closeEntry();
+            assertEquals(jarFile, IOUtils.getFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/"), false, null));
+            assertNull(IOUtils.getFileFromURL(new URL("jar:file:" + jarFile.getPath() + "!/test"), false, null));
+            File tmpJar = IOUtils.getFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test"), true, null);
+            assertNotNull(tmpJar);
+            assertNotEquals(jarFile, tmpJar);
+        } finally {
+            jarFile.delete();
         }
-
-        assertEquals(jarFile, IOUtils.getFileFromURL(new URL("jar:file:" + jarFile.getPath() + "!/"), false, null));
-        assertEquals(jarFile, IOUtils.getFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/"), false, null));
-        assertNull(IOUtils.getFileFromURL(new URL("jar:file:" + jarFile.getPath() + "!/test"), false, null));
-        File tmpJar = IOUtils.getFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test"), true, null);
-        assertNotNull(tmpJar);
-        assertNotEquals(jarFile, tmpJar);
     }
 
     @Test public void testGetJarFileFromURL() throws IOException {
-        File jarFile = File.createTempFile("\"IOUtilsTest \\\\\\\\+%23öäü^^^°$::\"", ".jar");
-
-        try (JarOutputStream output = new JarOutputStream(new FileOutputStream(jarFile))) {
-            output.putNextEntry(new JarEntry("test"));
-            output.write("Hello".getBytes());
-            output.closeEntry();
-            output.putNextEntry(new JarEntry("test.jar"));
-            try (JarOutputStream inner = new JarOutputStream(output)) {
-                inner.putNextEntry(new JarEntry("inner"));
-                inner.write("Hello".getBytes());
-                inner.closeEntry();
+        File jarFile = File.createTempFile("IOUtilsTest", ".jar");
+
+        try {
+            try (JarOutputStream output = new JarOutputStream(new FileOutputStream(jarFile))) {
+                output.putNextEntry(new JarEntry("test"));
+                output.write("Hello".getBytes());
+                output.closeEntry();
+                output.putNextEntry(new JarEntry("test.jar"));
+                try (JarOutputStream inner = new JarOutputStream(output)) {
+                    inner.putNextEntry(new JarEntry("inner"));
+                    inner.write("Hello".getBytes());
+                    inner.closeEntry();
+                }
             }
-        }
 
-        JarFile jar = IOUtils.getJarFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/"), true, null);
-        assertNotNull(jar);
-        jar = IOUtils.getJarFileFromURL(jarFile.toURI().toURL(), true, null);
-        assertNotNull(jar);
+            JarFile jar = IOUtils.getJarFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/"), true, null);
+            assertNotNull(jar);
+            jar = IOUtils.getJarFileFromURL(jarFile.toURI().toURL(), true, null);
+            assertNotNull(jar);
 
-        assertNull(IOUtils.getFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test"), false, null));
+            assertNull(IOUtils.getFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test"), false, null));
 
-        JarFile tmpJar = IOUtils.getJarFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test.jar"), true, null);
-        assertNotNull(tmpJar);
-        assertNotNull(tmpJar.getEntry("inner"));
+            JarFile tmpJar = IOUtils.getJarFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test.jar"), true, null);
+            assertNotNull(tmpJar);
+            assertNotNull(tmpJar.getEntry("inner"));
 
-        try {
-            IOUtils.getJarFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test"), true, null);
-            fail();
-        } catch (IOException ex) {
-            // Expected
-        }
+            try {
+                IOUtils.getJarFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test"), true, null);
+                fail();
+            } catch (IOException ex) {
+                // Expected
+            }
 
-        try {
-            IOUtils.getJarFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test.jar"), false, null);
-            fail();
-        } catch (IOException ex) {
-            // Expected
+            try {
+                IOUtils.getJarFileFromURL(new URL("jar:" + jarFile.toURI().toURL() + "!/test.jar"), false, null);
+                fail();
+            } catch (IOException ex) {
+                // Expected
+            }
+        } finally {
+            jarFile.delete();
         }
     }
 }
diff --git a/src/test/java/org/apache/sling/feature/io/artifacts/ArtifactManagerTest.java b/src/test/java/org/apache/sling/feature/io/artifacts/ArtifactManagerTest.java
index 5894734..fd106d3 100644
--- a/src/test/java/org/apache/sling/feature/io/artifacts/ArtifactManagerTest.java
+++ b/src/test/java/org/apache/sling/feature/io/artifacts/ArtifactManagerTest.java
@@ -26,9 +26,6 @@ import java.net.URL;
 import java.util.HashMap;
 import java.util.Map;
 
-import org.apache.sling.feature.io.artifacts.ArtifactHandler;
-import org.apache.sling.feature.io.artifacts.ArtifactManager;
-import org.apache.sling.feature.io.artifacts.ArtifactManagerConfig;
 import org.apache.sling.feature.io.artifacts.spi.ArtifactProvider;
 import org.junit.Test;