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

davidb pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/felix-dev.git


The following commit(s) were added to refs/heads/master by this push:
     new d381c9c  FELIX-6444 Contribute a compatible implementation of OSGi 
Features
     new 6eff0d0  Merge pull request #88 from bosschaert/features-contribution
d381c9c is described below

commit d381c9c52b2148606c79a1c10d410a1a2a98b9ea
Author: David Bosschaert <dav...@apache.org>
AuthorDate: Thu Aug 12 16:20:55 2021 +0100

    FELIX-6444 Contribute a compatible implementation of OSGi Features
    
    This implementation was initially made in the Apache Sling Whiteboard
    component at
    https://github.com/apache/sling-whiteboard/tree/master/osgi-featuremodel
---
 features/pom.xml                                   | 141 ++++++++
 .../felix/feature/impl/ArtifactBuilderImpl.java    |  95 +++++
 .../felix/feature/impl/BuilderFactoryImpl.java     |  59 +++
 .../felix/feature/impl/BundleBuilderImpl.java      |  95 +++++
 .../feature/impl/ConfigurationBuilderImpl.java     | 125 +++++++
 .../felix/feature/impl/ExtensionBuilderImpl.java   | 151 ++++++++
 .../felix/feature/impl/FeatureBuilderImpl.java     | 275 ++++++++++++++
 .../felix/feature/impl/FeatureServiceImpl.java     | 399 +++++++++++++++++++++
 .../java/org/apache/felix/feature/impl/IDImpl.java | 214 +++++++++++
 .../felix/feature/impl/FeatureServiceImplTest.java | 295 +++++++++++++++
 .../src/test/resources/features/test-exfeat1.json  |  26 ++
 .../src/test/resources/features/test-exfeat2.json  |   9 +
 .../src/test/resources/features/test-feature.json  |  28 ++
 .../src/test/resources/features/test-feature2.json |  19 +
 14 files changed, 1931 insertions(+)

diff --git a/features/pom.xml b/features/pom.xml
new file mode 100644
index 0000000..cd4f4d6
--- /dev/null
+++ b/features/pom.xml
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!-- 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. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+       xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+
+       <modelVersion>4.0.0</modelVersion>
+       <parent>
+               <groupId>org.apache.felix</groupId>
+               <artifactId>felix-parent</artifactId>
+               <version>7</version>
+               <relativePath />
+       </parent>
+
+       <artifactId>org.apache.felix.feature</artifactId>
+       <version>0.0.1-SNAPSHOT</version>
+       <packaging>jar</packaging>
+
+       <name>OSGi Feature Model API</name>
+
+    <properties>
+      <felix.java.version>11</felix.java.version>
+    </properties>
+        
+       <repositories>
+         <repository>
+           <id>sonatype.snapshots</id>
+           <name>OSGi Snapshot</name>
+           <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+         </repository>
+    </repositories>
+    
+       <build>
+               <plugins>
+            <plugin>
+                <groupId>biz.aQute.bnd</groupId>
+                <artifactId>bnd-maven-plugin</artifactId>
+                <version>5.3.0</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>bnd-process</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+                       <plugin>
+                               <groupId>org.apache.maven.plugins</groupId>
+                               <artifactId>maven-jar-plugin</artifactId>
+                               <configuration>
+                                       <archive>
+                                               
<manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
+                                       </archive>
+                               </configuration>
+                       </plugin>
+                       <plugin>
+                               <groupId>org.apache.rat</groupId>
+                               <artifactId>apache-rat-plugin</artifactId>
+                               <configuration>
+                                       <excludes>
+                                               <exclude>*.md</exclude>
+                                               
<exclude>src/main/resources/META-INF/services/*</exclude>
+                                       </excludes>
+                               </configuration>
+                       </plugin>
+               </plugins>
+       </build>
+       <dependencies>
+               <dependency>
+                       <groupId>org.osgi</groupId>
+                       <artifactId>osgi.annotation</artifactId>
+                       <version>8.0.0</version>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.osgi</groupId>
+                       <artifactId>osgi.core</artifactId>
+                       <version>8.0.0</version>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.osgi</groupId>
+                       <artifactId>org.osgi.service.feature</artifactId>
+                       <version>1.0.0-SNAPSHOT</version>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.osgi</groupId>
+                       <artifactId>org.osgi.util.function</artifactId>
+                       <version>1.0.0</version>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.apache.geronimo.specs</groupId>
+                       <artifactId>geronimo-json_1.1_spec</artifactId>
+                       <version>1.3</version>
+                       <scope>provided</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.apache.felix</groupId>
+                       <artifactId>org.apache.felix.converter</artifactId>
+                       <version>1.0.18</version>
+                       <scope>provided</scope>
+               </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.cm.json</artifactId>
+            <version>1.0.6</version>
+            <scope>provided</scope>
+        </dependency>
+
+               <!-- Testing -->
+               <dependency>
+                       <groupId>junit</groupId>
+                       <artifactId>junit</artifactId>
+                       <version>4.13.2</version>
+                       <scope>test</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.mockito</groupId>
+                       <artifactId>mockito-core</artifactId>
+                       <version>2.8.9</version>
+                       <scope>test</scope>
+               </dependency>
+               <dependency>
+                       <groupId>org.apache.johnzon</groupId>
+                       <artifactId>johnzon-core</artifactId>
+                       <version>1.2.2</version>
+                       <scope>test</scope>
+               </dependency>
+       </dependencies>
+</project>
diff --git 
a/features/src/main/java/org/apache/felix/feature/impl/ArtifactBuilderImpl.java 
b/features/src/main/java/org/apache/felix/feature/impl/ArtifactBuilderImpl.java
new file mode 100644
index 0000000..4fe6584
--- /dev/null
+++ 
b/features/src/main/java/org/apache/felix/feature/impl/ArtifactBuilderImpl.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations 
under
+ * the License.
+ */
+package org.apache.felix.feature.impl;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import org.osgi.service.feature.FeatureArtifact;
+import org.osgi.service.feature.FeatureArtifactBuilder;
+import org.osgi.service.feature.ID;
+
+class ArtifactBuilderImpl implements FeatureArtifactBuilder {
+    private final ID id;
+
+    private final Map<String,Object> metadata = new LinkedHashMap<>();
+
+    ArtifactBuilderImpl(ID id) {
+        this.id = id;
+    }
+
+    @Override
+    public FeatureArtifactBuilder addMetadata(String key, Object value) {
+        this.metadata.put(key, value);
+        return this;
+    }
+
+    @Override
+    public FeatureArtifactBuilder addMetadata(Map<String,Object> md) {
+        this.metadata.putAll(md);
+        return this;
+    }
+
+    @Override
+    public FeatureArtifact build() {
+        return new ArtifactImpl(id, metadata);
+    }
+
+    private static class ArtifactImpl implements FeatureArtifact {
+        private final ID id;
+        private final Map<String, Object> metadata;
+
+        private ArtifactImpl(ID id, Map<String, Object> metadata) {
+            this.id = id;
+            this.metadata = Collections.unmodifiableMap(metadata);
+        }
+
+        @Override
+        public ID getID() {
+            return id;
+        }
+        
+        @Override
+        public Map<String, Object> getMetadata() {
+            return metadata;
+        }
+
+        @Override
+               public int hashCode() {
+                       return Objects.hash(id, metadata);
+               }
+
+               @Override
+               public boolean equals(Object obj) {
+                       if (this == obj)
+                               return true;
+                       if (obj == null)
+                               return false;
+                       if (getClass() != obj.getClass())
+                               return false;
+                       ArtifactImpl other = (ArtifactImpl) obj;
+                       return Objects.equals(id, other.id) && 
Objects.equals(metadata, other.metadata);
+               }
+
+               @Override
+        public String toString() {
+            return "ArtifactImpl [getID()=" + getID() + "]";
+        }
+    }
+}
diff --git 
a/features/src/main/java/org/apache/felix/feature/impl/BuilderFactoryImpl.java 
b/features/src/main/java/org/apache/felix/feature/impl/BuilderFactoryImpl.java
new file mode 100644
index 0000000..7622aed
--- /dev/null
+++ 
b/features/src/main/java/org/apache/felix/feature/impl/BuilderFactoryImpl.java
@@ -0,0 +1,59 @@
+/*
+ * 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.felix.feature.impl;
+
+import org.osgi.service.feature.BuilderFactory;
+import org.osgi.service.feature.FeatureArtifactBuilder;
+import org.osgi.service.feature.FeatureBuilder;
+import org.osgi.service.feature.FeatureBundleBuilder;
+import org.osgi.service.feature.FeatureConfigurationBuilder;
+import org.osgi.service.feature.FeatureExtension.Kind;
+import org.osgi.service.feature.FeatureExtension.Type;
+import org.osgi.service.feature.FeatureExtensionBuilder;
+import org.osgi.service.feature.ID;
+
+class BuilderFactoryImpl implements BuilderFactory {
+       @Override
+       public FeatureArtifactBuilder newArtifactBuilder(ID id) {
+               return new ArtifactBuilderImpl(id);
+       }
+
+       @Override
+    public FeatureBundleBuilder newBundleBuilder(ID id) {
+        return new BundleBuilderImpl(id);
+    }
+
+    @Override
+    public FeatureConfigurationBuilder newConfigurationBuilder(String pid) {
+        return new ConfigurationBuilderImpl(pid);
+    }
+
+    @Override
+    public FeatureConfigurationBuilder newConfigurationBuilder(String 
factoryPid, String name) {
+        return new ConfigurationBuilderImpl(factoryPid, name);
+    }
+
+    @Override
+    public FeatureBuilder newFeatureBuilder(ID id) {
+        return new FeatureBuilderImpl(id);
+    }
+
+    @Override
+    public FeatureExtensionBuilder newExtensionBuilder(String name, Type type, 
Kind kind) {
+        return new ExtensionBuilderImpl(name, type, kind);
+    }
+}
diff --git 
a/features/src/main/java/org/apache/felix/feature/impl/BundleBuilderImpl.java 
b/features/src/main/java/org/apache/felix/feature/impl/BundleBuilderImpl.java
new file mode 100644
index 0000000..e19a2b1
--- /dev/null
+++ 
b/features/src/main/java/org/apache/felix/feature/impl/BundleBuilderImpl.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations 
under
+ * the License.
+ */
+package org.apache.felix.feature.impl;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+import org.osgi.service.feature.FeatureBundle;
+import org.osgi.service.feature.FeatureBundleBuilder;
+import org.osgi.service.feature.ID;
+
+class BundleBuilderImpl implements FeatureBundleBuilder {
+    private final ID id;
+
+    private final Map<String,Object> metadata = new LinkedHashMap<>();
+
+    BundleBuilderImpl(ID id) {
+        this.id = id;
+    }
+
+    @Override
+    public FeatureBundleBuilder addMetadata(String key, Object value) {
+        this.metadata.put(key, value);
+        return this;
+    }
+
+    @Override
+    public FeatureBundleBuilder addMetadata(Map<String,Object> md) {
+        this.metadata.putAll(md);
+        return this;
+    }
+
+    @Override
+    public FeatureBundle build() {
+        return new BundleImpl(id, metadata);
+    }
+
+    private static class BundleImpl implements FeatureBundle {
+        private final ID id;
+        private final Map<String, Object> metadata;
+
+        private BundleImpl(ID id, Map<String, Object> metadata) {
+            this.id = id;
+            this.metadata = Collections.unmodifiableMap(metadata);
+        }
+
+        @Override
+        public ID getID() {
+            return id;
+        }
+        
+        @Override
+        public Map<String, Object> getMetadata() {
+            return metadata;
+        }
+
+        @Override
+               public int hashCode() {
+                       return Objects.hash(id, metadata);
+               }
+
+               @Override
+               public boolean equals(Object obj) {
+                       if (this == obj)
+                               return true;
+                       if (obj == null)
+                               return false;
+                       if (getClass() != obj.getClass())
+                               return false;
+                       BundleImpl other = (BundleImpl) obj;
+                       return Objects.equals(id, other.id) && 
Objects.equals(metadata, other.metadata);
+               }
+
+        @Override
+        public String toString() {
+            return "BundleImpl [getID()=" + getID() + "]";
+        }
+    }
+}
diff --git 
a/features/src/main/java/org/apache/felix/feature/impl/ConfigurationBuilderImpl.java
 
b/features/src/main/java/org/apache/felix/feature/impl/ConfigurationBuilderImpl.java
new file mode 100644
index 0000000..62da552
--- /dev/null
+++ 
b/features/src/main/java/org/apache/felix/feature/impl/ConfigurationBuilderImpl.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations 
under
+ * the License.
+ */
+package org.apache.felix.feature.impl;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.osgi.service.feature.FeatureConfiguration;
+import org.osgi.service.feature.FeatureConfigurationBuilder;
+
+class ConfigurationBuilderImpl implements FeatureConfigurationBuilder {
+    private final String p;
+    private final String name;
+
+    private final Map<String,Object> values = new LinkedHashMap<>();
+
+    ConfigurationBuilderImpl(String pid) {
+        this.p = pid;
+        this.name = null;
+    }
+
+    ConfigurationBuilderImpl(String factoryPid, String name) {
+        this.p = factoryPid;
+        this.name = name;
+    }
+
+    ConfigurationBuilderImpl(FeatureConfiguration c) {
+        if (c.getFactoryPid() == null) {
+            p = c.getPid();
+            name = null;
+        } else {
+            // TODO
+            p = null;
+            name = null;
+        }
+
+        addValues(c.getValues());
+    }
+
+    @Override
+    public FeatureConfigurationBuilder addValue(String key, Object value) {
+        // TODO can do some validation on the configuration
+        this.values.put(key, value);
+        return this;
+    }
+
+    @Override
+    public FeatureConfigurationBuilder addValues(Map<String, Object> cfg) {
+        // TODO can do some validation on the configuration
+        this.values.putAll(cfg);
+        return this;
+    }
+
+    @Override
+    public FeatureConfiguration build() {
+        if (name == null) {
+            return new ConfigurationImpl(p, null, values);
+        } else {
+            return new ConfigurationImpl(p + "~" + name, p, values);
+        }
+    }
+
+    private static class ConfigurationImpl implements FeatureConfiguration {
+        private final String pid;
+        private final Optional<String> factoryPid;
+        private final Map<String, Object> values;
+
+        private ConfigurationImpl(String pid, String factoryPid,
+                Map<String, Object> values) {
+            this.pid = pid;
+            this.factoryPid = Optional.ofNullable(factoryPid);
+            this.values = Collections.unmodifiableMap(values);
+        }
+
+        public String getPid() {
+            return pid;
+        }
+
+        public Optional<String> getFactoryPid() {
+            return factoryPid;
+        }
+
+        public Map<String, Object> getValues() {
+            return values;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(factoryPid, pid, values);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj)
+                return true;
+            if (!(obj instanceof ConfigurationImpl))
+                return false;
+            ConfigurationImpl other = (ConfigurationImpl) obj;
+            return Objects.equals(factoryPid, other.factoryPid) && 
Objects.equals(pid, other.pid)
+                    && Objects.equals(values, other.values);
+        }
+
+        @Override
+        public String toString() {
+            return "ConfigurationImpl [pid=" + pid + "]";
+        }
+    }
+}
diff --git 
a/features/src/main/java/org/apache/felix/feature/impl/ExtensionBuilderImpl.java
 
b/features/src/main/java/org/apache/felix/feature/impl/ExtensionBuilderImpl.java
new file mode 100644
index 0000000..81da44c
--- /dev/null
+++ 
b/features/src/main/java/org/apache/felix/feature/impl/ExtensionBuilderImpl.java
@@ -0,0 +1,151 @@
+/*
+ * 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.felix.feature.impl;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.osgi.service.feature.FeatureArtifact;
+import org.osgi.service.feature.FeatureExtension;
+import org.osgi.service.feature.FeatureExtension.Kind;
+import org.osgi.service.feature.FeatureExtension.Type;
+import org.osgi.service.feature.FeatureExtensionBuilder;
+
+class ExtensionBuilderImpl implements FeatureExtensionBuilder {
+    private final String name;
+    private final Type type;
+    private final Kind kind;
+
+    private final List<String> content = new ArrayList<>();
+    private final List<FeatureArtifact> artifacts = new ArrayList<>();
+
+    ExtensionBuilderImpl(String name, Type type, Kind kind) {
+        this.name = name;
+        this.type = type;
+        this.kind = kind;
+    }
+
+    @Override
+    public FeatureExtensionBuilder addText(String text) {
+        if (type != Type.TEXT)
+            throw new IllegalStateException("Cannot add text to extension of 
type " + type);
+
+        content.add(text);
+        return this;
+    }
+
+    @Override
+    public FeatureExtensionBuilder setJSON(String json) {
+        if (type != Type.JSON)
+            throw new IllegalStateException("Cannot add text to extension of 
type " + type);
+
+        content.clear(); // Clear any previous value
+        content.add(json);
+        return this;
+    }
+
+    @Override
+    public FeatureExtensionBuilder addArtifact(FeatureArtifact art) {
+        if (type != Type.ARTIFACTS)
+            throw new IllegalStateException("Cannot add artifacts to extension 
of type " + type);
+
+        artifacts.add(art);
+        return this;
+    }
+    
+    @Override
+    public FeatureExtension build() {
+        return new ExtensionImpl(name, type, kind, content, artifacts);
+    }
+
+    private static class ExtensionImpl implements FeatureExtension {
+        private final String name;
+        private final Type type;
+        private final Kind kind;
+        private final List<String> content;
+        private final List<FeatureArtifact> artifacts;
+
+        private ExtensionImpl(String name, Type type, Kind kind, List<String> 
content, List<FeatureArtifact> artifacts) {
+            this.name = name;
+            this.type = type;
+            this.kind = kind;
+            this.content = Collections.unmodifiableList(content);
+            this.artifacts = Collections.unmodifiableList(artifacts);
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public Type getType() {
+            return type;
+        }
+
+        public Kind getKind() {
+            return kind;
+        }
+
+        public String getJSON() {
+            if (type != Type.JSON)
+                throw new IllegalStateException("Extension is not of type JSON 
" + type);
+
+            if (content.isEmpty())
+                return null;
+
+            return content.get(0);
+        }
+
+        public List<String> getText() {
+            if (type != Type.TEXT)
+                throw new IllegalStateException("Extension is not of type Text 
" + type);
+
+            return content;
+        }
+
+        public List<FeatureArtifact> getArtifacts() {
+            if (type != Type.ARTIFACTS)
+                throw new IllegalStateException("Extension is not of type Text 
" + type);
+
+            return artifacts;
+        }
+
+        @Override
+               public int hashCode() {
+                       return Objects.hash(artifacts, content, kind, name, 
type);
+               }
+
+        @Override
+               public boolean equals(Object obj) {
+                       if (this == obj)
+                               return true;
+                       if (obj == null)
+                               return false;
+                       if (getClass() != obj.getClass())
+                               return false;
+                       ExtensionImpl other = (ExtensionImpl) obj;
+                       return Objects.equals(artifacts, other.artifacts) && 
Objects.equals(content, other.content)
+                                       && kind == other.kind && 
Objects.equals(name, other.name) && type == other.type;
+               }
+
+               @Override
+        public String toString() {
+            return "ExtensionImpl [name=" + name + ", type=" + type + "]";
+        }
+    }
+}
diff --git 
a/features/src/main/java/org/apache/felix/feature/impl/FeatureBuilderImpl.java 
b/features/src/main/java/org/apache/felix/feature/impl/FeatureBuilderImpl.java
new file mode 100644
index 0000000..2f86860
--- /dev/null
+++ 
b/features/src/main/java/org/apache/felix/feature/impl/FeatureBuilderImpl.java
@@ -0,0 +1,275 @@
+/*
+ * 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.felix.feature.impl;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+import org.osgi.service.feature.Feature;
+import org.osgi.service.feature.FeatureBuilder;
+import org.osgi.service.feature.FeatureBundle;
+import org.osgi.service.feature.FeatureConfiguration;
+import org.osgi.service.feature.FeatureExtension;
+import org.osgi.service.feature.ID;
+
+class FeatureBuilderImpl implements FeatureBuilder {
+    private final ID id;
+
+    private String name;
+    private String description;
+    private String docURL;
+    private String license;
+    private String scm;
+    private String vendor;
+    private boolean complete;
+
+    private final List<FeatureBundle> bundles = new ArrayList<>();
+    private final List<String> categories = new ArrayList<>();
+    private final Map<String,FeatureConfiguration> configurations = new 
LinkedHashMap<>();
+    private final Map<String,FeatureExtension> extensions = new 
LinkedHashMap<>();
+    private final Map<String,String> variables = new LinkedHashMap<>();
+
+    FeatureBuilderImpl(ID id) {
+        this.id = id;
+    }
+
+    @Override
+    public FeatureBuilder setName(String name) {
+        this.name = name;
+        return this;
+    }
+
+    @Override
+    public FeatureBuilder setDocURL(String url) {
+        this.docURL = url;
+        return this;
+    }
+
+    @Override
+    public FeatureBuilder setVendor(String vendor) {
+        this.vendor = vendor;
+        return this;
+    }
+
+    @Override
+    public FeatureBuilder setLicense(String license) {
+        this.license = license;
+        return this;
+    }
+
+    @Override
+    public FeatureBuilder setComplete(boolean complete) {
+        this.complete = complete;
+        return this;
+    }
+
+    @Override
+    public FeatureBuilder setDescription(String description) {
+        this.description = description;
+        return this;
+    }
+
+    @Override
+    public FeatureBuilder setSCM(String scm) {
+        this.scm = scm;
+        return this;
+    }
+
+    @Override
+    public FeatureBuilder addBundles(FeatureBundle ... bundles) {
+        this.bundles.addAll(Arrays.asList(bundles));
+        return this;
+    }
+
+    
+    @Override
+       public FeatureBuilder addCategories(String ...categories) {
+       this.categories.addAll(Arrays.asList(categories));
+               return this;
+       }
+
+       @Override
+    public FeatureBuilder addConfigurations(FeatureConfiguration ... configs) {
+        for (FeatureConfiguration cfg : configs) {
+            this.configurations.put(cfg.getPid(), cfg);
+        }
+        return this;
+    }
+
+    @Override
+    public FeatureBuilder addExtensions(FeatureExtension ... extensions) {
+        for (FeatureExtension ex : extensions) {
+            this.extensions.put(ex.getName(), ex);
+        }
+        return this;
+    }
+
+    @Override
+    public FeatureBuilder addVariable(String key, String value) {
+        this.variables.put(key, value);
+        return this;
+    }
+
+    @Override
+    public FeatureBuilder addVariables(Map<String,String> variables) {
+        this.variables.putAll(variables);
+        return this;
+    }
+
+    @Override
+    public Feature build() {
+        return new FeatureImpl(id, name, description, docURL,
+                license, scm, vendor, complete,
+                bundles, categories, configurations, extensions, variables);
+    }
+
+    private static class FeatureImpl implements Feature {
+        private final ID id;
+        private final Optional<String> name;
+        private final Optional<String> description;
+        private final Optional<String> docURL;
+        private final Optional<String> license;
+        private final Optional<String> scm;
+        private final Optional<String> vendor;
+        private final boolean complete;
+
+        private final List<FeatureBundle> bundles;
+        private final List<String> categories;
+        private final Map<String,FeatureConfiguration> configurations;
+        private final Map<String,FeatureExtension> extensions;
+        private final Map<String,String> variables;
+
+        private FeatureImpl(ID id, String aName, String desc, String docs, 
String lic, String sc, String vnd,
+                boolean comp, List<FeatureBundle> bs, List<String> cats, 
Map<String,FeatureConfiguration> cs,
+                Map<String,FeatureExtension> es, Map<String,String> vars) {
+            this.id = id;
+            name = Optional.ofNullable(aName);
+            description = Optional.ofNullable(desc);
+            docURL = Optional.ofNullable(docs);
+            license = Optional.ofNullable(lic);
+            scm = Optional.ofNullable(sc);
+            vendor = Optional.ofNullable(vnd);
+            complete = comp;
+
+            bundles = Collections.unmodifiableList(bs);
+            categories = Collections.unmodifiableList(cats);
+            configurations = Collections.unmodifiableMap(cs);
+            extensions = Collections.unmodifiableMap(es);
+            variables = Collections.unmodifiableMap(vars);
+        }
+
+        @Override
+        public ID getID() {
+            return id;
+        }
+        
+        @Override
+        public Optional<String> getName() {
+            return name;
+        }
+
+        @Override
+        public Optional<String> getDescription() {
+            return description;
+        }
+
+        @Override
+        public Optional<String> getVendor() {
+            return vendor;
+        }
+
+        @Override
+        public Optional<String> getLicense() {
+            return license;
+        }
+
+        @Override
+        public Optional<String> getDocURL() {
+            return docURL;
+        }
+
+        @Override
+        public Optional<String> getSCM() {
+            return scm;
+        }
+
+        @Override
+        public boolean isComplete() {
+            return complete;
+        }
+
+        @Override
+        public List<FeatureBundle> getBundles() {
+            return bundles;
+        }
+
+        @Override
+        public List<String> getCategories() {
+            return categories;
+        }
+
+        @Override
+        public Map<String,FeatureConfiguration> getConfigurations() {
+            return configurations;
+        }
+
+        @Override
+        public Map<String,FeatureExtension> getExtensions() {
+            return extensions;
+        }
+
+        @Override
+        public Map<String,String> getVariables() {
+            return variables;
+        }
+
+        @Override
+               public int hashCode() {
+                       return Objects.hash(bundles, categories, complete, 
configurations, description, docURL,
+                                       extensions, id, license, name, scm, 
variables, vendor);
+               }
+
+               @Override
+               public boolean equals(Object obj) {
+                       if (this == obj)
+                               return true;
+                       if (obj == null)
+                               return false;
+                       if (getClass() != obj.getClass())
+                               return false;
+                       FeatureImpl other = (FeatureImpl) obj;
+                       return Objects.equals(bundles, other.bundles) && 
Objects.equals(categories, other.categories)
+                                       && complete == other.complete && 
Objects.equals(configurations, other.configurations)
+                                       && Objects.equals(description, 
other.description)
+                                       && Objects.equals(docURL, other.docURL) 
&& Objects.equals(extensions, other.extensions)
+                                       && Objects.equals(id, other.id) && 
Objects.equals(license, other.license)
+                                       && Objects.equals(name, other.name) && 
Objects.equals(scm, other.scm)
+                                       && Objects.equals(variables, 
other.variables) && Objects.equals(vendor, other.vendor);
+               }
+
+               @Override
+        public String toString() {
+            return "FeatureImpl [getID()=" + getID() + "]";
+        }
+    }
+}
diff --git 
a/features/src/main/java/org/apache/felix/feature/impl/FeatureServiceImpl.java 
b/features/src/main/java/org/apache/felix/feature/impl/FeatureServiceImpl.java
new file mode 100644
index 0000000..10dc999
--- /dev/null
+++ 
b/features/src/main/java/org/apache/felix/feature/impl/FeatureServiceImpl.java
@@ -0,0 +1,399 @@
+/*
+ * 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.felix.feature.impl;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;  
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonArrayBuilder;
+import javax.json.JsonNumber;
+import javax.json.JsonObject;
+import javax.json.JsonObjectBuilder;
+import javax.json.JsonString;
+import javax.json.JsonValue;
+import javax.json.stream.JsonGenerator;
+import javax.json.stream.JsonGeneratorFactory;
+
+import org.apache.felix.cm.json.impl.JsonSupport;
+import org.apache.felix.cm.json.impl.TypeConverter;
+import org.osgi.service.feature.BuilderFactory;
+import org.osgi.service.feature.Feature;
+import org.osgi.service.feature.FeatureArtifact;
+import org.osgi.service.feature.FeatureArtifactBuilder;
+import org.osgi.service.feature.FeatureBuilder;
+import org.osgi.service.feature.FeatureBundle;
+import org.osgi.service.feature.FeatureBundleBuilder;
+import org.osgi.service.feature.FeatureConfiguration;
+import org.osgi.service.feature.FeatureConfigurationBuilder;
+import org.osgi.service.feature.FeatureExtension;
+import org.osgi.service.feature.FeatureExtensionBuilder;
+import org.osgi.service.feature.FeatureService;
+import org.osgi.service.feature.ID;
+
+public class FeatureServiceImpl implements FeatureService {
+    private final BuilderFactoryImpl builderFactory = new BuilderFactoryImpl();
+
+    public BuilderFactory getBuilderFactory() {
+        return builderFactory;
+    }
+    
+    @Override
+       public ID getIDfromMavenCoordinates(String mavenID) {
+       return IDImpl.fromMavenID(mavenID);
+       }
+
+       @Override
+       public ID getID(String groupId, String artifactId, String version) {
+               return new IDImpl(groupId, artifactId, version, null, null);
+       }
+
+       @Override
+       public ID getID(String groupId, String artifactId, String version, 
String type) {
+               return new IDImpl(groupId, artifactId, version, type, null);
+       }
+
+       @Override
+       public ID getID(String groupId, String artifactId, String version, 
String type, String classifier) {
+               return new IDImpl(groupId, artifactId, version, type, 
classifier);
+       }
+
+       public Feature readFeature(Reader jsonReader) throws IOException {
+        JsonObject json = Json.createReader(
+                       
JsonSupport.createCommentRemovingReader(jsonReader)).readObject();
+
+        String id = json.getString("id");
+        FeatureBuilder builder = 
builderFactory.newFeatureBuilder(getIDfromMavenCoordinates(id));
+
+        builder.setName(json.getString("name", null));
+        builder.setDescription(json.getString("description", null));
+        builder.setDocURL(json.getString("docURL", null));
+        builder.setLicense(json.getString("license", null));
+        builder.setSCM(json.getString("scm", null));
+        builder.setVendor(json.getString("vendor", null));
+
+        builder.setComplete(json.getBoolean("complete", false));
+
+        builder.addBundles(getBundles(json));
+        builder.addCategories(getCategories(json));
+        builder.addConfigurations(getConfigurations(json));
+        builder.addExtensions(getExtensions(json));
+
+        return builder.build();
+    }
+
+    private FeatureBundle[] getBundles(JsonObject json) {
+        JsonArray ja = json.getJsonArray("bundles");
+        if (ja == null)
+            return new FeatureBundle[] {};
+
+        List<FeatureBundle> bundles = new ArrayList<>();
+
+        for (JsonValue val : ja) {
+            if (val.getValueType() == JsonValue.ValueType.OBJECT) {
+                JsonObject jo = val.asJsonObject();
+                String bid = jo.getString("id");
+                FeatureBundleBuilder builder = 
builderFactory.newBundleBuilder(getIDfromMavenCoordinates(bid));
+
+                for (Map.Entry<String, JsonValue> entry : jo.entrySet()) {
+                    if (entry.getKey().equals("id"))
+                        continue;
+
+                    JsonValue value = entry.getValue();
+
+                    Object v;
+                    switch (value.getValueType()) {
+                    case NUMBER:
+                        v = ((JsonNumber) value).longValueExact();
+                        break;
+                    case STRING:
+                        v = ((JsonString) value).getString();
+                        break;
+                    default:
+                        v = value.toString();
+                    }
+                    builder.addMetadata(entry.getKey(), v);
+                }
+                bundles.add(builder.build());
+            }
+        }
+
+        return bundles.toArray(new FeatureBundle[0]);
+    }
+
+    private String[] getCategories(JsonObject json) {
+        JsonArray ja = json.getJsonArray("categories");
+        if (ja == null)
+            return new String[] {};
+
+        List<String> cats = ja.getValuesAs(JsonString::getString);
+        return cats.toArray(new String[] {});
+    }
+
+    private FeatureConfiguration[] getConfigurations(JsonObject json) {
+        JsonObject jo = json.getJsonObject("configurations");
+        if (jo == null)
+            return new FeatureConfiguration[] {};
+
+        List<FeatureConfiguration> configs = new ArrayList<>();
+
+        for (Map.Entry<String, JsonValue> entry : jo.entrySet()) {
+
+            String p = entry.getKey();
+            String factoryPid = null;
+            int idx = p.indexOf('~');
+            if (idx > 0) {
+                factoryPid = p.substring(0, idx);
+                p = p.substring(idx + 1);
+            }
+
+            FeatureConfigurationBuilder builder;
+            if (factoryPid == null) {
+                builder = builderFactory.newConfigurationBuilder(p);
+            } else {
+                builder = builderFactory.newConfigurationBuilder(factoryPid, 
p);
+            }
+
+            JsonObject values = entry.getValue().asJsonObject();
+            for (Map.Entry<String, JsonValue> value : values.entrySet()) {
+               String key = value.getKey();
+               String typeInfo = null;
+               int cidx = key.indexOf(':');
+               if (cidx > 0) {
+                       typeInfo = key.substring(cidx + 1);
+                       key = key.substring(0, cidx);
+               }
+               
+                JsonValue val = value.getValue();
+                // TODO ensure that binary support works as well
+                Object v = TypeConverter.convertObjectToType(val, typeInfo);   
             
+                builder.addValue(key, v);
+            }
+            configs.add(builder.build());
+        }
+
+        return configs.toArray(new FeatureConfiguration[] {});
+    }
+
+    private FeatureExtension[] getExtensions(JsonObject json) {
+        JsonObject jo = json.getJsonObject("extensions");
+        if (jo == null)
+            return new FeatureExtension[] {};
+
+        List<FeatureExtension> extensions = new ArrayList<>();
+
+        for (Map.Entry<String,JsonValue> entry : jo.entrySet()) {
+            JsonObject exData = entry.getValue().asJsonObject();
+            FeatureExtension.Type type;
+            if (exData.containsKey("text")) {
+                type = FeatureExtension.Type.TEXT;
+            } else if (exData.containsKey("artifacts")) {
+                type = FeatureExtension.Type.ARTIFACTS;
+            } else if (exData.containsKey("json")) {
+                type = FeatureExtension.Type.JSON;
+            } else {
+                throw new IllegalStateException("Invalid extension: " + entry);
+            }
+            String k = exData.getString("kind", "optional");
+            FeatureExtension.Kind kind = 
FeatureExtension.Kind.valueOf(k.toUpperCase());
+
+            FeatureExtensionBuilder builder = 
builderFactory.newExtensionBuilder(entry.getKey(), type, kind);
+
+            switch (type) {
+            case TEXT:
+                exData.getJsonArray("text")
+                       .stream()
+                       .filter(jv -> jv.getValueType() == 
JsonValue.ValueType.STRING)
+                       .map(jv -> ((JsonString) jv).getString())
+                       .forEach(builder::addText);
+                
+                break;
+            case ARTIFACTS:
+               exData.getJsonArray("artifacts")
+                       .stream()
+                       .filter(jv -> jv.getValueType() == 
JsonValue.ValueType.OBJECT)
+                       .map(jv -> (JsonObject) jv)
+                       .forEach(md -> {
+                               Map<String, JsonValue> v = new HashMap<>(md);
+                               JsonString idVal = (JsonString) v.remove("id");
+                               
+                               ID id = 
getIDfromMavenCoordinates(idVal.getString());
+                               FeatureArtifactBuilder fab = 
builderFactory.newArtifactBuilder(id);
+                               
+                               for (Map.Entry<String,JsonValue> mde : 
v.entrySet()) {
+                                       JsonValue val = mde.getValue();
+                                       switch (val.getValueType()) {
+                                       case STRING:
+                                               fab.addMetadata(mde.getKey(), 
((JsonString) val).getString());
+                                               break;
+                                       case FALSE:
+                                               fab.addMetadata(mde.getKey(), 
false);
+                                               break;
+                                       case TRUE:
+                                               fab.addMetadata(mde.getKey(), 
true);
+                                               break;
+                                       case NUMBER:
+                                               JsonNumber num = (JsonNumber) 
val;
+                                               if 
(num.toString().contains(".")) {
+                                                       
fab.addMetadata(mde.getKey(), num.doubleValue());                               
                        
+                                               } else {
+                                                       
fab.addMetadata(mde.getKey(), num.longValue());
+                                               }
+                                               break;
+                                       default:
+                                               // do nothing
+                                               break;
+                                       }
+                               }
+                               
+                               builder.addArtifact(fab.build());
+                       });
+
+               break;
+            case JSON:
+                builder.setJSON(exData.getJsonObject("json").toString());
+                break;
+            }
+            extensions.add(builder.build());
+        }
+
+        return extensions.toArray(new FeatureExtension[] {});
+    }
+
+    public void writeFeature(Feature feature, Writer jsonWriter) throws 
IOException {
+       // LinkedHashMap to give it some order, we'd like 'id' and 'name' first.
+       Map<String,Object> attrs = new LinkedHashMap<>();
+       
+       attrs.put("id", feature.getID().toString());
+       feature.getName().ifPresent(n -> attrs.put("name", n));
+       feature.getDescription().ifPresent(d -> attrs.put("description", d));
+       feature.getDocURL().ifPresent(d -> attrs.put("docURL", d));
+       feature.getLicense().ifPresent(l -> attrs.put("license", l));
+       feature.getSCM().ifPresent(s -> attrs.put("scm", s));
+       feature.getVendor().ifPresent(v -> attrs.put("vendor", v));
+       
+               JsonObjectBuilder json = Json.createObjectBuilder(attrs);
+
+               JsonArray bundles = getBundles(feature);
+               if (bundles != null) {
+                       json.add("bundles", bundles);
+               }
+               
+               JsonObject configs = getConfigurations(feature);
+               if (configs != null) {
+                       json.add("configurations", configs);
+               }
+               
+               JsonObject extensions = getExtensions(feature);
+               if (extensions != null) {
+                       json.add("extensions", extensions);
+               }
+               
+               // TODO add variables
+               // TODO add frameworkproperties
+               
+               JsonObject fo = json.build();
+               
+               JsonGeneratorFactory gf = 
Json.createGeneratorFactory(Collections.singletonMap(JsonGenerator.PRETTY_PRINTING,
 true));
+               try (JsonGenerator gr = gf.createGenerator(jsonWriter)) {
+                       gr.write(fo);
+               }
+    }
+
+       private JsonArray getBundles(Feature feature) {
+               List<FeatureBundle> bundles = feature.getBundles();
+               if (bundles == null || bundles.size() == 0)
+                       return null;
+               
+               JsonArrayBuilder ab = Json.createArrayBuilder();
+               
+               for (FeatureBundle bundle : bundles) {
+                       Map<String, Object> attrs = new LinkedHashMap<>();
+                       attrs.put("id", bundle.getID().toString());
+                       attrs.putAll(bundle.getMetadata());
+                       ab.add(Json.createObjectBuilder(attrs));
+               }
+               
+               return ab.build();
+       }
+
+       private JsonObject getConfigurations(Feature feature) {
+               Map<String, FeatureConfiguration> configs = 
feature.getConfigurations();
+               if (configs == null || configs.size() == 0)
+                       return null;
+               
+               JsonObjectBuilder ob = Json.createObjectBuilder();
+               
+               for (Map.Entry<String,FeatureConfiguration> cfg : 
configs.entrySet()) {
+                       JsonObjectBuilder cb = Json.createObjectBuilder();
+                       
+                       for (Map.Entry<String,Object> prop : 
cfg.getValue().getValues().entrySet()) {
+                               Map.Entry<String, JsonValue> je = 
TypeConverter.convertObjectToTypedJsonValue(prop.getValue());
+                               String tk = je.getKey();
+                               cb.add(TypeConverter.NO_TYPE_INFO.equals(tk) ? 
prop.getKey() : prop.getKey() + ":" + tk, je.getValue());
+                       }
+                       ob.add(cfg.getKey(), cb.build());
+               }
+               return ob.build();
+       }
+
+       private JsonObject getExtensions(Feature feature) {
+               Map<String, FeatureExtension> extensions = 
feature.getExtensions();
+               if (extensions == null || extensions.size() == 0)
+                       return null;
+               
+               JsonObjectBuilder ob = Json.createObjectBuilder();
+               
+               for (Map.Entry<String,FeatureExtension> entry : 
extensions.entrySet()) {
+                       FeatureExtension extVal = entry.getValue();
+
+                       JsonObjectBuilder vb = Json.createObjectBuilder();
+                       vb.add("kind", 
extVal.getKind().toString().toLowerCase());
+                       
+                       switch (extVal.getType()) {
+                       case TEXT:
+                               vb.add("text", 
Json.createArrayBuilder(extVal.getText()).build());
+                               break;
+                       case ARTIFACTS:
+                               JsonArrayBuilder arr = 
Json.createArrayBuilder();
+                               for (FeatureArtifact art : 
extVal.getArtifacts()) {
+                                       Map<String,Object> attrs = new 
LinkedHashMap<>();
+                                       attrs.put("id", art.getID().toString());
+                                       attrs.putAll(art.getMetadata());
+                                       
arr.add(Json.createObjectBuilder(attrs)).build();
+                               }
+                               
+                               vb.add("artifacts", arr.build());
+                               break;
+                       case JSON:
+                               vb.add("json", Json.createReader(new 
StringReader(extVal.getJSON())).readValue());
+                               break;
+                       }
+                       ob.add(entry.getKey(), vb.build());
+               }
+               return ob.build();
+       }
+}
diff --git a/features/src/main/java/org/apache/felix/feature/impl/IDImpl.java 
b/features/src/main/java/org/apache/felix/feature/impl/IDImpl.java
new file mode 100644
index 0000000..76ef473
--- /dev/null
+++ b/features/src/main/java/org/apache/felix/feature/impl/IDImpl.java
@@ -0,0 +1,214 @@
+/*
+ * 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.felix.feature.impl;
+
+import java.util.Objects;
+import java.util.Optional;
+
+import org.osgi.service.feature.ID;
+
+public class IDImpl implements ID {
+    private final String groupId;
+    private final String artifactId;
+    private final String version; // The Artifact Version may not follow OSGi 
version rules
+    private final String type;
+    private final String classifier;
+
+    /**
+        * Construct an ID from a Maven ID. Maven IDs have the following syntax:
+        * <p>
+        * {@code group-id ':' artifact-id [ ':' [type] [ ':' classifier ] ] 
':' version}
+        *
+        * @param mavenID
+        * @return The ID
+        * @throws IllegalArgumentException if the mavenID does not match the 
Syntax
+        */
+       public static IDImpl fromMavenID(String mavenID)
+                       throws IllegalArgumentException {
+        String[] parts = mavenID.split(":");
+
+        if (parts.length < 3 || parts.length > 5)
+            throw new IllegalArgumentException("Not a valid maven ID" + 
mavenID);
+
+        String gid = parts[0];
+        String aid = parts[1];
+               String ver = null;
+               String t = null;
+               String c = null;
+
+               if (parts.length == 3) {
+                       ver = parts[2];
+               } else if (parts.length == 4) {
+                       t = parts[2];
+                       ver = parts[3];
+               } else {
+                       t = parts[2];
+                       c = parts[3];
+                       ver = parts[4];
+               }
+        return new IDImpl(gid, aid, ver, t, c);
+    }
+
+    /**
+        * Construct an ID
+        * 
+        * @param groupId The group ID.
+        * @param artifactId The artifact ID.
+        * @param version The version.
+        * @param type The type identifier.
+        * @param classifier The classifier.
+        * @throws NullPointerException if one of the parameters (groupId,
+        *             artifactId, version) is null.
+        * @throws IllegalArgumentException if one of the parameters is empty or
+        *             contains an colon `:` or if a classifier is used without 
a
+        *             type.
+        */
+       public IDImpl(String groupId, String artifactId, String version, String 
type,
+                       String classifier)
+                       throws NullPointerException, IllegalArgumentException {
+
+               Objects.requireNonNull(groupId, "groupId");
+               Objects.requireNonNull(artifactId, "artifact");
+               Objects.requireNonNull(version, "version");
+
+               if (groupId.isEmpty()) {
+                       throw new IllegalArgumentException("groupId must not be 
empty");
+               }
+               if (artifactId.isEmpty()) {
+                       throw new IllegalArgumentException("artifactId must not 
be empty");
+               }
+               if (version.isEmpty()) {
+                       throw new IllegalArgumentException("version must not be 
empty");
+               }
+
+               if (type != null && type.isEmpty()) {
+                       throw new IllegalArgumentException("type must not be 
empty");
+               }
+
+               if (classifier != null && classifier.isEmpty()) {
+                       throw new IllegalArgumentException("classifier must not 
be empty");
+               }
+
+               if (groupId.contains(":")) {
+                       throw new IllegalArgumentException(
+                                       "groupId must not contain a colon `:`");
+               }
+               if (artifactId.contains(":")) {
+                       throw new IllegalArgumentException(
+                                       "artifactId must not contain a colon 
`:`");
+               }
+               if (version.contains(":")) {
+                       throw new IllegalArgumentException(
+                                       "version must not contain a colon `:`");
+               }
+               if (type != null && type.contains(":")) {
+                       throw new IllegalArgumentException(
+                                       "type must not contain a colon `:`");
+               }
+               if (classifier != null && classifier.contains(":")) {
+                       throw new IllegalArgumentException(
+                                       "classifier must not contain a colon 
`:`");
+               }
+               if (type == null && classifier != null) {
+                       throw new IllegalArgumentException(
+                                       "type must not be `null` if a 
classifier is set");
+               }
+               this.groupId = groupId;
+               this.artifactId = artifactId;
+               this.version = version;
+               this.type = type;
+               this.classifier = classifier;
+    }
+
+    /**
+     * Get the group ID.
+     * @return The group ID.
+     */
+    public String getGroupId() {
+        return groupId;
+    }
+
+    /**
+     * Get the artifact ID.
+     * @return The artifact ID.
+     */
+    public String getArtifactId() {
+        return artifactId;
+    }
+
+    /**
+     * Get the version.
+     * @return The version.
+     */
+    public String getVersion() {
+        return version;
+    }
+
+    /**
+     * Get the type identifier.
+     * @return The type identifier.
+     */
+    public Optional<String> getType() {
+        return Optional.ofNullable(type);
+    }
+
+    /**
+     * Get the classifier.
+     * @return The classifier.
+     */
+    public Optional<String> getClassifier() {
+        return Optional.ofNullable(classifier);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(artifactId, classifier, groupId, type, version);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!(obj instanceof IDImpl))
+            return false;
+        IDImpl other = (IDImpl) obj;
+        return Objects.equals(artifactId, other.artifactId) && 
Objects.equals(classifier, other.classifier)
+                && Objects.equals(groupId, other.groupId) && 
Objects.equals(type, other.type)
+                && Objects.equals(version, other.version);
+    }
+
+       /**
+        * Returns the the mavenID. Maven IDs have the following syntax:
+        * <p>
+        * {@code group-id ':' artifact-id [ ':' [type] [ ':' classifier ] ] 
':' version}
+        * 
+        * @return the mavenID.
+        */
+    @Override
+    public String toString() {
+               StringBuilder sb = new StringBuilder(groupId).append(":")
+                               .append(artifactId);
+
+               if (type != null) {
+                       sb = sb.append(":").append(type);
+                       if (classifier != null) {
+                               sb = sb.append(":").append(classifier);
+                       }
+               }
+               return sb.append(":").append(version).toString();
+    }
+}
diff --git 
a/features/src/test/java/org/apache/felix/feature/impl/FeatureServiceImplTest.java
 
b/features/src/test/java/org/apache/felix/feature/impl/FeatureServiceImplTest.java
new file mode 100644
index 0000000..9b2334b
--- /dev/null
+++ 
b/features/src/test/java/org/apache/felix/feature/impl/FeatureServiceImplTest.java
@@ -0,0 +1,295 @@
+/*
+ * 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.felix.feature.impl;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.net.URL;
+import java.util.List;
+import java.util.Map;
+
+import javax.json.Json;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+
+import org.apache.felix.feature.impl.FeatureServiceImpl;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.service.feature.BuilderFactory;
+import org.osgi.service.feature.Feature;
+import org.osgi.service.feature.FeatureArtifact;
+import org.osgi.service.feature.FeatureBuilder;
+import org.osgi.service.feature.FeatureBundle;
+import org.osgi.service.feature.FeatureConfiguration;
+import org.osgi.service.feature.FeatureExtension;
+import org.osgi.service.feature.FeatureService;
+
+public class FeatureServiceImplTest {
+       FeatureServiceImpl features;
+
+    @Before
+    public void setUp() {
+        features = new FeatureServiceImpl();
+    }
+
+    @Test
+    public void testReadFeature() throws IOException {
+        BuilderFactory bf = features.getBuilderFactory();
+
+        URL res = getClass().getResource("/features/test-feature.json");
+        
+        Feature f;
+        try (Reader r = new InputStreamReader(res.openStream())) {
+            f = features.readFeature(r);
+
+            assertTrue(f.getName().isEmpty());
+            assertEquals("The feature description", f.getDescription().get());
+            assertFalse(f.getDocURL().isPresent());
+            assertFalse(f.getLicense().isPresent());
+            assertFalse(f.getSCM().isPresent());
+            assertFalse(f.getVendor().isPresent());
+
+            List<FeatureBundle> bundles = f.getBundles();
+            assertEquals(3, bundles.size());
+
+            FeatureBundle bundle = 
bf.newBundleBuilder(features.getID("org.osgi", "osgi.promise", "7.0.1"))
+                    .addMetadata("hash", "4632463464363646436")
+                    .addMetadata("start-order", 1L)
+                    .build();
+
+            FeatureBundle ba = bundles.get(0);
+            ba.equals(bundle);
+
+            assertTrue(bundles.contains(bundle));
+            
assertTrue(bundles.contains(bf.newBundleBuilder(features.getID("org.slf4j", 
"slf4j-api", "1.7.29")).build()));
+            
assertTrue(bundles.contains(bf.newBundleBuilder(features.getID("org.slf4j", 
"slf4j-simple", "1.7.29")).build()));
+            
+            Map<String, FeatureConfiguration> configs = f.getConfigurations();
+            assertEquals(2, configs.size());
+            
+            FeatureConfiguration cfg1 = configs.get("my.pid");
+            assertEquals("my.pid", cfg1.getPid());
+            assertFalse(cfg1.getFactoryPid().isPresent());
+            Map<String, Object> values1 = cfg1.getValues();
+            assertEquals(3, values1.size());
+            assertEquals(Long.valueOf(5), values1.get("foo"));
+            assertEquals("test", values1.get("bar"));
+            assertEquals(Integer.valueOf(7), values1.get("number"));
+            
+            FeatureConfiguration cfg2 = configs.get("my.factory.pid~name");
+            assertEquals("my.factory.pid~name", cfg2.getPid());
+            assertEquals("my.factory.pid", cfg2.getFactoryPid().get());
+            Map<String, Object> values2 = cfg2.getValues();
+            assertEquals(1, values2.size());
+            assertArrayEquals(new String[] {"yeah", "yeah", "yeah"}, 
(String[]) values2.get("a.value"));
+        }
+
+        testWriteFeature(f, res);
+    }
+    
+    @Test
+    public void testReadFeature2() throws Exception {
+        URL res = getClass().getResource("/features/test-feature2.json");
+        try (Reader r = new InputStreamReader(res.openStream())) {
+            Feature f = features.readFeature(r);
+
+            
assertEquals("org.apache.sling:test-feature2:osgifeature:cls_abc:1.1", 
f.getID().toString());
+            assertEquals("test-feature2", f.getName().get());
+            assertEquals("The feature description", f.getDescription().get());
+            assertEquals(List.of("foo", "bar"), f.getCategories());
+            assertEquals("http://foo.bar.com/abc";, f.getDocURL().get());
+            assertEquals("Apache-2.0; 
link=\"http://opensource.org/licenses/apache2.0.php\"";, f.getLicense().get());
+            assertEquals("url=https://github.com/apache/sling-aggregator, 
connection=scm:git:https://github.com/apache/sling-aggregator.git, 
developerConnection=scm:git:g...@github.com:apache/sling-aggregator.git", 
+                       f.getSCM().get());
+            assertEquals("The Apache Software Foundation", 
f.getVendor().get());
+        }      
+    }
+
+    @Test
+    public void testWriteFeature() throws Exception {
+        BuilderFactory factory = features.getBuilderFactory();
+
+        String desc = "This is the main ACME app, from where all functionality 
can be reached.";
+
+        FeatureBuilder builder = 
factory.newFeatureBuilder(features.getID("org.acme", "acmeapp", "1.0.0"));
+        builder.setName("The ACME app");
+               builder.setDescription(desc);
+
+        Feature f = builder.build();
+        StringWriter sw = new StringWriter();
+        features.writeFeature(f, sw);
+        
+        // Now check the generated JSON
+        JsonReader jr = Json.createReader(new StringReader(sw.toString()));
+        JsonObject fo = jr.readObject();
+        assertEquals("org.acme:acmeapp:1.0.0", fo.getString("id"));
+        assertEquals("The ACME app", fo.getString("name"));
+        assertEquals(desc, fo.getString("description"));
+        assertFalse(fo.containsKey("docURL"));
+        assertFalse(fo.containsKey("license"));
+        assertFalse(fo.containsKey("scm"));
+        assertFalse(fo.containsKey("vendor"));
+    }
+
+    @Test
+    public void testFeatureWithExtension() throws Exception {
+        URL res = getClass().getResource("/features/test-exfeat1.json");
+        Feature f;
+        try (Reader r = new InputStreamReader(res.openStream())) {
+            f = features.readFeature(r);
+            
+            Map<String, FeatureExtension> extensions = f.getExtensions();
+            assertEquals(3, extensions.size());
+            
+            FeatureExtension textEx = extensions.get("my-text-ex");
+            assertEquals(FeatureExtension.Kind.OPTIONAL, textEx.getKind());
+            assertEquals(FeatureExtension.Type.TEXT, textEx.getType());
+            assertEquals(List.of("ABC", "DEF"), textEx.getText());
+            
+            FeatureExtension artEx = extensions.get("my-art-ex");
+            assertEquals(FeatureExtension.Kind.MANDATORY, artEx.getKind());
+            assertEquals(FeatureExtension.Type.ARTIFACTS, artEx.getType());
+            List<FeatureArtifact> arts = artEx.getArtifacts();
+            assertEquals(2, arts.size());
+            
+            FeatureArtifact art1 = arts.get(0);
+            assertEquals("g:a:1", art1.getID().toString());
+            assertEquals(1, art1.getMetadata().size());
+            assertEquals(12345L, art1.getMetadata().get("my-md"));
+            
+            FeatureArtifact art2 = arts.get(1);
+            assertEquals("g:a:zip:foobar:2", art2.getID().toString());
+            assertEquals(0, art2.getMetadata().size());
+            
+            FeatureExtension jsonEx = extensions.get("my-json-ex");
+            assertEquals(FeatureExtension.Kind.TRANSIENT, jsonEx.getKind());
+            assertEquals(FeatureExtension.Type.JSON, jsonEx.getType());
+            assertEquals("{\"foo\":[1,2,3]}", jsonEx.getJSON());
+        }      
+        
+        testWriteFeature(f, res);
+    }
+
+       private void testWriteFeature(Feature feature, URL expectedURL) throws 
IOException {
+               StringWriter sw = new StringWriter();
+        features.writeFeature(feature, sw);
+        
+        String expected = new 
String(expectedURL.openStream().readAllBytes()).replaceAll("\\s","");
+        String actual = sw.toString().replaceAll("\\s","");
+        assertEquals(expected, actual);
+       }
+    
+    @Test
+    public void testCreateFeatureBundle() {
+        BuilderFactory factory = features.getBuilderFactory();
+
+        FeatureBuilder builder = factory.newFeatureBuilder(
+                       features.getID("org.acme", "acmeapp", "1.0.1"));
+        builder.setName("The Acme Application");
+        builder.setLicense("https://opensource.org/licenses/Apache-2.0";);
+        builder.setComplete(true);
+
+        FeatureBundle b1 = factory.newBundleBuilder(
+                
features.getIDfromMavenCoordinates("org.osgi:org.osgi.util.function:1.1.0"))
+                .build();
+        FeatureBundle b2 = factory.newBundleBuilder(
+                       
features.getIDfromMavenCoordinates("org.osgi:org.osgi.util.promise:1.1.1"))
+                .build();
+
+        FeatureBundle b3 = factory.newBundleBuilder(
+                       
features.getIDfromMavenCoordinates("org.apache.commons:commons-email:1.1.5"))
+                .addMetadata("org.acme.javadoc.link",
+                        
"https://commons.apache.org/proper/commons-email/javadocs/api-1.5";)
+                .build();
+
+        FeatureBundle b4 = factory.newBundleBuilder(
+                       
features.getIDfromMavenCoordinates("com.acme:acmelib:1.7.2"))
+                .build();
+
+        builder.addBundles(b1, b2, b3, b4);
+
+        Feature f = builder.build();
+        System.out.println("***" + f);
+    }
+    
+       @Test
+       public void testCreateFeature() {
+               FeatureService fs = features;
+               BuilderFactory factory = fs.getBuilderFactory();
+               
+               FeatureBuilder builder = 
factory.newFeatureBuilder(fs.getID("org.acme", "acmeapp", "1.0.0"));
+               builder.setName("The ACME app");
+               builder.setDescription("This is the main ACME app, from where 
all functionality can be reached.");
+               Feature f = builder.build();
+               
+               
assertEquals(fs.getIDfromMavenCoordinates("org.acme:acmeapp:1.0.0"), f.getID());
+               assertEquals("The ACME app", f.getName().get());
+               assertEquals("This is the main ACME app, from where all 
functionality can be reached.", f.getDescription().get());
+       }
+       
+       @Test
+       public void testCreateFeatureWithBundles() {
+               FeatureService fs = features;
+               BuilderFactory factory = fs.getBuilderFactory();
+               FeatureBuilder builder = 
factory.newFeatureBuilder(fs.getID("org.acme", "acmeapp", "1.0.1"));
+               builder.setName("The Acme Application");
+               
builder.setLicense("https://opensource.org/licenses/Apache-2.0";);
+               builder.setComplete(true);
+               FeatureBundle b1 = factory
+                               
.newBundleBuilder(fs.getIDfromMavenCoordinates("org.osgi:org.osgi.util.function:1.1.0")).build();
+               FeatureBundle b2 = factory
+                               
.newBundleBuilder(fs.getIDfromMavenCoordinates("org.osgi:org.osgi.util.promise:1.1.1")).build();
+               FeatureBundle b3 = factory
+                               
.newBundleBuilder(fs.getIDfromMavenCoordinates("org.apache.commons:commons-email:1.1.5"))
+                               .addMetadata("org.acme.javadoc.link",
+                                               
"https://commons.apache.org/proper/commons-email/javadocs/api-1.5";)
+                               .build();
+               FeatureBundle b4 = 
factory.newBundleBuilder(fs.getIDfromMavenCoordinates("com.acme:acmelib:1.7.2")).build();
+               builder.addBundles(b1, b2, b3, b4);
+               Feature f = builder.build();
+               
+               assertEquals("https://opensource.org/licenses/Apache-2.0";, 
f.getLicense().get());
+               assertTrue(f.isComplete());
+               
+               assertEquals(4, f.getBundles().size());
+               
+               FeatureBundle fb1 = f.getBundles().get(0);
+               assertEquals("org.osgi:org.osgi.util.function:1.1.0", 
fb1.getID().toString());
+               assertEquals(0, fb1.getMetadata().size());
+               
+               FeatureBundle fb2 = f.getBundles().get(1);
+               assertEquals("org.osgi:org.osgi.util.promise:1.1.1", 
fb2.getID().toString());
+               assertEquals(0, fb2.getMetadata().size());
+
+               FeatureBundle fb3 = f.getBundles().get(2);
+               assertEquals("org.apache.commons:commons-email:1.1.5", 
fb3.getID().toString());
+               assertEquals(1, fb3.getMetadata().size());
+               
assertEquals("https://commons.apache.org/proper/commons-email/javadocs/api-1.5";,
 fb3.getMetadata().get("org.acme.javadoc.link"));
+
+               FeatureBundle fb4 = f.getBundles().get(3);
+               assertEquals("com.acme:acmelib:1.7.2", fb4.getID().toString());
+               assertEquals(0, fb4.getMetadata().size());
+       }
+}
diff --git a/features/src/test/resources/features/test-exfeat1.json 
b/features/src/test/resources/features/test-exfeat1.json
new file mode 100644
index 0000000..7112392
--- /dev/null
+++ b/features/src/test/resources/features/test-exfeat1.json
@@ -0,0 +1,26 @@
+{
+    "id" : "org.apache.sling:test-extension-feature:1.0.0",
+    "extensions" : {
+        "my-text-ex": {
+            "kind": "optional",
+            "text": [
+               "ABC", "DEF"
+               ] 
+        },
+        "my-art-ex": {
+            "kind": "mandatory", 
+            "artifacts": [
+               {
+                                       "id": "g:a:1",
+                                       "my-md": 12345
+                               }, {
+                                       "id": "g:a:zip:foobar:2"
+                               }
+                       ]
+        },
+        "my-json-ex": {
+           "kind": "transient", 
+            "json": {"foo": [1, 2, 3] }
+        }
+    }
+}
\ No newline at end of file
diff --git a/features/src/test/resources/features/test-exfeat2.json 
b/features/src/test/resources/features/test-exfeat2.json
new file mode 100644
index 0000000..5fa93e4
--- /dev/null
+++ b/features/src/test/resources/features/test-exfeat2.json
@@ -0,0 +1,9 @@
+{
+    "id" : "org.apache.sling:test-extension-feature2:1.0.0",
+    "extensions" : {
+        "my-text-ex": 
+        {
+            "text": "DEF"
+        }
+    }
+}
\ No newline at end of file
diff --git a/features/src/test/resources/features/test-feature.json 
b/features/src/test/resources/features/test-feature.json
new file mode 100644
index 0000000..323fb79
--- /dev/null
+++ b/features/src/test/resources/features/test-feature.json
@@ -0,0 +1,28 @@
+{
+    "id" : "org.apache.sling:test-feature:1.1",
+    "description": "The feature description",
+
+    "bundles" :[
+            {
+              "id" : "org.osgi:osgi.promise:7.0.1",
+              "hash" : "4632463464363646436",
+              "start-order" : 1
+            },
+            {
+              "id" : "org.slf4j:slf4j-api:1.7.29"
+            },
+            {
+              "id" : "org.slf4j:slf4j-simple:1.7.29"
+            }
+    ],
+    "configurations" : {
+        "my.pid" : {
+           "foo" : 5,
+           "bar" : "test",
+           "number:Integer" : 7
+        },
+        "my.factory.pid~name" : {
+           "a.value" : ["yeah", "yeah", "yeah"]
+        }
+    }
+}
\ No newline at end of file
diff --git a/features/src/test/resources/features/test-feature2.json 
b/features/src/test/resources/features/test-feature2.json
new file mode 100644
index 0000000..e12d2d8
--- /dev/null
+++ b/features/src/test/resources/features/test-feature2.json
@@ -0,0 +1,19 @@
+{
+    /** This is a JSMin comment */
+    "id" : "org.apache.sling:test-feature2:osgifeature:cls_abc:1.1",
+    "name": "test-feature2",
+    "description": "The feature description",
+    "categories": ["foo", "bar"],
+    "docURL": "http://foo.bar.com/abc";,
+    "license": "Apache-2.0; 
link=\"http://opensource.org/licenses/apache2.0.php\"";,
+    "scm": "url=https://github.com/apache/sling-aggregator, 
connection=scm:git:https://github.com/apache/sling-aggregator.git, 
developerConnection=scm:git:g...@github.com:apache/sling-aggregator.git",
+    "vendor": "The Apache Software Foundation",
+
+       // complex entities below here
+       
+    "configurations" : {
+        "my.pid" : {
+           "bar" : "toast"
+        }
+    }
+}
\ No newline at end of file

Reply via email to