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 2022/03/13 14:09:15 UTC

[sling-org-apache-sling-feature] branch master updated: SLING-11199 : Support OSGi Feature Implementation

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


The following commit(s) were added to refs/heads/master by this push:
     new 47e790f  SLING-11199 : Support OSGi Feature Implementation
47e790f is described below

commit 47e790fba6dfe78c6eafc44fa59dcc43de3cf8ef
Author: Carsten Ziegeler <cz...@apache.org>
AuthorDate: Sun Mar 13 15:09:05 2022 +0100

    SLING-11199 : Support OSGi Feature Implementation
---
 bnd.bnd                                            |   1 +
 pom.xml                                            |  14 +-
 .../org/apache/sling/feature/osgi/Converters.java  | 322 +++++++++++++++++++++
 .../apache/sling/feature/osgi/package-info.java    |  23 ++
 .../apache/sling/feature/osgi/ConvertersTest.java  | 305 +++++++++++++++++++
 5 files changed, 664 insertions(+), 1 deletion(-)

diff --git a/bnd.bnd b/bnd.bnd
index e65563c..501d090 100644
--- a/bnd.bnd
+++ b/bnd.bnd
@@ -1,2 +1,3 @@
+Import-Package: org.osgi.service.feature;resolution:=dynamic,*
 -noimportjava: true
 -conditionalpackage: org.apache.felix.utils.*
diff --git a/pom.xml b/pom.xml
index 38a9174..acde3aa 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
     </parent>
 
     <artifactId>org.apache.sling.feature</artifactId>
-    <version>1.2.31-SNAPSHOT</version>
+    <version>1.3.0-SNAPSHOT</version>
 
     <name>Apache Sling Feature Model</name>
     <description>
@@ -105,6 +105,12 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.feature</artifactId>
+            <version>1.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>org.apache.felix</groupId>
             <artifactId>org.apache.felix.utils</artifactId>
             <version>1.11.8</version>
@@ -141,6 +147,12 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.feature</artifactId>
+            <version>0.9.4-RC3</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.apache.johnzon</groupId>
             <artifactId>johnzon-core</artifactId>
             <version>1.2.14</version>
diff --git a/src/main/java/org/apache/sling/feature/osgi/Converters.java b/src/main/java/org/apache/sling/feature/osgi/Converters.java
new file mode 100755
index 0000000..4169914
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/osgi/Converters.java
@@ -0,0 +1,322 @@
+/*
+ * 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.osgi;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.ServiceLoader;
+
+import org.apache.felix.cm.json.Configurations;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+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;
+import org.osgi.service.feature.FeatureExtension.Kind;
+import org.osgi.service.feature.FeatureExtension.Type;
+
+/**
+ * Utility class to convert Apache Sling features to OSGi feature and vice versa.
+ */
+public class Converters {
+
+    /** Use the first available feature service */
+    private static final FeatureService service = ServiceLoader.load(FeatureService.class).iterator().next();
+
+    /** Constant for framework launching properties extension */
+    private static final String FRAMEWORK_PROPERTIES_EXTENSION = "framework-launching-properties";
+
+    /** Constant for framework properties metadata */
+    private static final String FRAMEWORK_PROPERTIES_METADATA = "framework-properties-metadata";
+
+    /** Constant for metadata of variables */
+    private static final String VARIABLES_METADATA = "variables-metadata";
+
+    /**
+     * Convert an Apache Sling feature into an OSGi feature
+     * @param feature The feature to convert
+     * @return The OSGi feature or {@code null} if feature is {@code null}
+     * @throws IOException If the conversion fails
+     */
+    public static Feature convert(final org.apache.sling.feature.Feature feature) throws IOException {
+        if ( feature == null ) {
+            return null;
+        }
+        final ID id = service.getIDfromMavenCoordinates(feature.getId().toMvnId());
+        final FeatureBuilder builder = service.getBuilderFactory().newFeatureBuilder(id);
+
+        // metadata
+        builder.setComplete(feature.isComplete());
+        builder.setDescription(feature.getDescription());
+        builder.setLicense(feature.getLicense());
+        builder.setName(feature.getTitle());
+        builder.setVendor(feature.getVendor());
+        builder.setDocURL(feature.getDocURL());
+        builder.setSCM(feature.getSCMInfo());
+        for(final String v : feature.getCategories()) {
+            builder.addCategories(v);
+        }
+
+        // variables
+        feature.getVariables().entrySet().stream().forEach(entry -> builder.addVariable(entry.getKey(), entry.getValue()));
+
+        // bundles
+        for(final Artifact bundle : feature.getBundles()) {
+            final FeatureBundleBuilder b = service.getBuilderFactory().newBundleBuilder(service.getIDfromMavenCoordinates(bundle.getId().toMvnId()));
+            bundle.getMetadata().entrySet().stream().forEach(entry -> b.addMetadata(entry.getKey(), entry.getValue()));
+            builder.addBundles(b.build());
+        }
+
+        // configurations
+        for(final Configuration cfg : feature.getConfigurations()) {
+            final FeatureConfigurationBuilder b;
+            if ( cfg.isFactoryConfiguration() ) {
+                b = service.getBuilderFactory().newConfigurationBuilder(cfg.getFactoryPid(), cfg.getName());
+            } else {
+                b = service.getBuilderFactory().newConfigurationBuilder(cfg.getPid());
+            }
+            for(final String name : Collections.list(cfg.getProperties().keys()) ) {
+                b.addValue(name, cfg.getProperties().get(name));
+            }
+            builder.addConfigurations(b.build());
+        }
+
+        // extensions
+        for(final Extension ext : feature.getExtensions()) {
+            FeatureExtension.Type type;
+            if ( ext.getType() == ExtensionType.ARTIFACTS ) {
+                type = Type.ARTIFACTS;
+            } else if ( ext.getType() == ExtensionType.TEXT ) {
+                type = Type.TEXT;
+            } else {
+                type = Type.JSON;
+            }
+            FeatureExtension.Kind kind;
+            if ( ext.getState() == ExtensionState.OPTIONAL ) {
+                kind = Kind.OPTIONAL;
+            } else if ( ext.getState() == ExtensionState.REQUIRED ) {
+                kind = Kind.MANDATORY;
+            } else {
+                kind = Kind.TRANSIENT;
+            }
+            final FeatureExtensionBuilder b = service.getBuilderFactory().newExtensionBuilder(ext.getName(), type, kind);
+            if ( ext.getType() == ExtensionType.ARTIFACTS ) {
+                for(final Artifact artifact : ext.getArtifacts()) {
+                    final FeatureArtifactBuilder ab = service.getBuilderFactory().newArtifactBuilder(service.getIDfromMavenCoordinates(artifact.getId().toMvnId()));
+                    artifact.getMetadata().entrySet().stream().forEach(entry -> ab.addMetadata(entry.getKey(), entry.getValue()));
+                    b.addArtifact(ab.build());
+                }
+            } else if ( ext.getType() == ExtensionType.TEXT ) {
+                if ( ext.getText() != null ) {
+                    for(final String t : ext.getText().split("\n")) {
+                        b.addText(t);
+                    }
+                }
+            } else {
+                if ( ext.getJSON() != null ) {
+                    b.setJSON(ext.getJSON());
+                } else {
+                    b.setJSON("{}");
+                }
+            }
+            builder.addExtensions(b.build());            
+        }
+
+        // framework properties
+        if ( ! feature.getFrameworkProperties().isEmpty() ) {
+            final FeatureExtensionBuilder b = service.getBuilderFactory().newExtensionBuilder(FRAMEWORK_PROPERTIES_EXTENSION, Type.JSON, Kind.MANDATORY);
+            final Dictionary<String, Object> properties = new Hashtable<>();
+            feature.getFrameworkProperties().entrySet().stream().forEach(entry -> properties.put(entry.getKey(), entry.getValue()));
+            try ( final Writer writer = new StringWriter()) {
+                Configurations.buildWriter().build(writer).writeConfiguration(properties);
+                writer.flush();
+                b.setJSON(writer.toString());
+            }
+            builder.addExtensions(b.build());
+        }
+        
+        // Write metadata for variables and framework properties in the internal extension
+        final Hashtable<String, Object> output = Configurations.newConfiguration();
+        if ( !feature.getFrameworkProperties().isEmpty() ) {
+            final Map<String, Object> fwkMetadata = Configurations.newConfiguration();
+            for(final String fwkPropName : feature.getFrameworkProperties().keySet()) {
+                final Map<String, Object> metadata = feature.getFrameworkPropertyMetadata(fwkPropName);
+                if ( !metadata.isEmpty() ) {
+                    fwkMetadata.put(fwkPropName, metadata);
+                }
+            }
+            if ( !fwkMetadata.isEmpty() ) {
+                output.put(FRAMEWORK_PROPERTIES_METADATA, fwkMetadata);
+            }
+        }
+        if ( !feature.getVariables().isEmpty() ) {
+            final Map<String, Object> varMetadata = Configurations.newConfiguration();
+            for(final String varName : feature.getVariables().keySet()) {
+                final Map<String, Object> metadata = feature.getVariableMetadata(varName);
+                if ( !metadata.isEmpty() ) {
+                    varMetadata.put(varName, metadata);
+                }
+            }
+            if ( !varMetadata.isEmpty() ) {
+                output.put(VARIABLES_METADATA, varMetadata);
+            }
+        }
+        if ( !output.isEmpty() ) {
+            final FeatureExtensionBuilder b = service.getBuilderFactory().newExtensionBuilder(Extension.EXTENSION_NAME_INTERNAL_DATA, 
+                    Type.JSON, Kind.OPTIONAL);
+            try ( final Writer writer = new StringWriter()) {
+                Configurations.buildWriter().build(writer).writeConfiguration(output);
+                writer.flush();
+                b.setJSON(writer.toString());
+            }
+            builder.addExtensions(b.build());
+        }
+
+        return builder.build();
+    }
+
+    /**
+     * Convert an OSGi feature into an Apache Sling feature
+     * @param feature The feature to convert
+     * @return The Apache Sling feature or {@code null} if feature is {@code null}
+     * @throws IOException If the conversion fails
+     */
+    public static org.apache.sling.feature.Feature convert(final Feature feature) throws IOException {
+        if ( feature == null ) {
+            return null;
+        }
+        final org.apache.sling.feature.Feature f = new org.apache.sling.feature.Feature(ArtifactId.parse(feature.getID().toString()));
+
+        // metadata
+        f.setComplete(feature.isComplete());
+        f.setDescription(feature.getDescription().orElse(null));
+        f.setLicense(feature.getLicense().orElse(null));
+        f.setTitle(feature.getName().orElse(null));
+        f.setVendor(feature.getVendor().orElse(null));
+        f.setDocURL(feature.getDocURL().orElse(null));
+        f.setSCMInfo(feature.getSCM().orElse(null));
+        f.getCategories().addAll(feature.getCategories());
+
+        // variables
+        feature.getVariables().entrySet().stream().forEach(entry -> f.getVariables().put(entry.getKey(), entry.getValue().toString()));
+
+        // bundles
+        for(final FeatureBundle bundle : feature.getBundles()) {
+            final Artifact b = new Artifact(ArtifactId.parse(bundle.getID().toString()));
+            bundle.getMetadata().entrySet().stream().forEach(entry -> b.getMetadata().put(entry.getKey(), entry.getValue().toString()));
+            f.getBundles().add(b);
+        }
+
+        // configurations
+        for(final FeatureConfiguration cfg : feature.getConfigurations().values()) {
+            final Configuration c = new Configuration(cfg.getPid());
+            cfg.getValues().entrySet().stream().forEach(entry -> c.getProperties().put(entry.getKey(), entry.getValue()));
+            f.getConfigurations().add(c);
+        }
+
+        // extensions
+        for(final FeatureExtension ext : feature.getExtensions().values()) {
+            if ( FRAMEWORK_PROPERTIES_EXTENSION.equals(ext.getName()) ) {
+                // framework properties
+                try ( final Reader reader = new StringReader(ext.getJSON())) {
+                    Configurations.buildReader().build(reader).readConfiguration().entrySet()
+                            .stream().forEach(entry -> f.getFrameworkProperties().put(entry.getKey(), entry.getValue().toString()));
+                }
+            } else if ( Extension.EXTENSION_NAME_INTERNAL_DATA.equals(ext.getName()) ) {
+                // metadata for variables and framework properties
+                try ( final Reader reader = new StringReader(ext.getJSON())) {
+                    final Hashtable<String, Object> md = Configurations.buildReader().build(reader).readConfiguration();
+
+                    final String varMetadata = (String) md.get(VARIABLES_METADATA);
+                    if ( varMetadata != null ) {
+                        try ( final StringReader r = new StringReader(varMetadata)) {
+                            for(final Map.Entry<String, Hashtable<String, Object>> entry : Configurations.buildReader()
+                                    .verifyAsBundleResource(true)
+                                    .build(r)
+                                    .readConfigurationResource().getConfigurations().entrySet()) {
+                                f.getVariableMetadata(entry.getKey()).putAll(entry.getValue());
+                            }
+                        }
+                    }
+        
+                    final String fwkMetadata = (String) md.get(FRAMEWORK_PROPERTIES_METADATA);
+                    if ( fwkMetadata != null ) {
+                        try ( final StringReader r = new StringReader(fwkMetadata)) {
+                            for(final Map.Entry<String, Hashtable<String, Object>> entry : Configurations.buildReader()
+                                    .verifyAsBundleResource(true)
+                                    .build(r)
+                                    .readConfigurationResource().getConfigurations().entrySet()) {
+                                f.getFrameworkPropertyMetadata(entry.getKey()).putAll(entry.getValue());
+                            }
+                        }
+                    }
+                }
+            } else {
+                ExtensionType type;
+                if ( ext.getType() == Type.ARTIFACTS ) {
+                    type = ExtensionType.ARTIFACTS;
+                } else if ( ext.getType() == Type.TEXT ) {
+                    type = ExtensionType.TEXT;
+                } else {
+                    type = ExtensionType.JSON;
+                }
+                ExtensionState state;
+                if ( ext.getKind() == Kind.OPTIONAL ) {
+                    state = ExtensionState.OPTIONAL;
+                } else if ( ext.getKind() == Kind.MANDATORY ) {
+                    state = ExtensionState.REQUIRED;
+                } else {
+                    state = ExtensionState.TRANSIENT;
+                }
+                final Extension e = new Extension(type, ext.getName(), state);
+                if ( ext.getType() == Type.ARTIFACTS ) {
+                    for(final FeatureArtifact artifact : ext.getArtifacts()) {
+                        final Artifact a = new Artifact(ArtifactId.parse(artifact.getID().toString()));
+                        artifact.getMetadata().entrySet().stream().forEach(entry -> a.getMetadata().put(entry.getKey(), entry.getValue().toString()));
+                        e.getArtifacts().add(a);
+                    }
+                } else if ( ext.getType() == Type.TEXT ) {
+                    e.setText(String.join("\n", ext.getText()));
+                } else {
+                    e.setJSON(ext.getJSON());
+                }
+                f.getExtensions().add(e);       
+            }
+        }
+        return f;
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/osgi/package-info.java b/src/main/java/org/apache/sling/feature/osgi/package-info.java
new file mode 100644
index 0000000..d756741
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/osgi/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.sling.feature.osgi;
+
+
diff --git a/src/test/java/org/apache/sling/feature/osgi/ConvertersTest.java b/src/test/java/org/apache/sling/feature/osgi/ConvertersTest.java
new file mode 100644
index 0000000..5d538a3
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/osgi/ConvertersTest.java
@@ -0,0 +1,305 @@
+/*
+ * 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.osgi;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.StringReader;
+import java.util.Arrays;
+import java.util.Hashtable;
+
+import org.apache.felix.cm.json.Configurations;
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionState;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Feature;
+import org.junit.Test;
+
+public class ConvertersTest {
+    
+    @Test public void testEmptyFeatureConversion() throws Exception {
+        final Feature feature = new Feature(ArtifactId.parse("g:a:1"));
+
+        // convert to OSGi feature
+        final org.osgi.service.feature.Feature osgiFeature = Converters.convert(feature);
+        assertEquals("g:a:1", osgiFeature.getID().toString());
+        assertFalse(osgiFeature.getDescription().isPresent());
+        assertFalse(osgiFeature.getDocURL().isPresent());
+        assertFalse(osgiFeature.getLicense().isPresent());
+        assertFalse(osgiFeature.getSCM().isPresent());
+        assertFalse(osgiFeature.getName().isPresent());
+        assertFalse(osgiFeature.getVendor().isPresent());
+        assertFalse(osgiFeature.isComplete());
+        assertTrue(osgiFeature.getCategories().isEmpty());
+        assertTrue(osgiFeature.getBundles().isEmpty());
+        assertTrue(osgiFeature.getConfigurations().isEmpty());
+        assertTrue(osgiFeature.getExtensions().isEmpty());
+        assertTrue(osgiFeature.getVariables().isEmpty());
+
+        // and back to Sling Feature
+        final Feature slingFeature = Converters.convert(osgiFeature);
+        assertEquals("g:a:1", slingFeature.getId().toMvnId());
+        assertNull(slingFeature.getDescription());
+        assertNull(slingFeature.getDocURL());
+        assertNull(slingFeature.getLicense());
+        assertNull(slingFeature.getSCMInfo());
+        assertNull(slingFeature.getTitle());
+        assertNull(slingFeature.getVendor());
+        assertFalse(slingFeature.isComplete());
+        assertTrue(slingFeature.getCategories().isEmpty());
+        assertTrue(slingFeature.getBundles().isEmpty());
+        assertTrue(slingFeature.getConfigurations().isEmpty());
+        assertTrue(slingFeature.getExtensions().isEmpty());
+        assertTrue(slingFeature.getVariables().isEmpty());
+    }
+
+    @Test public void testMetadataConversion() throws Exception {
+        final Feature feature = new Feature(ArtifactId.parse("g:a:1"));
+        feature.setComplete(true);
+        feature.setDescription("description");
+        feature.setDocURL("doc-url");
+        feature.setLicense("license");
+        feature.setSCMInfo("info");
+        feature.setTitle("title");
+        feature.setVendor("vendor");
+        feature.getCategories().add("c1");
+        feature.getCategories().add("c2");
+
+        // convert to OSGi feature
+        final org.osgi.service.feature.Feature osgiFeature = Converters.convert(feature);
+        assertEquals("g:a:1", osgiFeature.getID().toString());
+        assertEquals("description", osgiFeature.getDescription().get());
+        assertEquals("doc-url", osgiFeature.getDocURL().get());
+        assertEquals("license", osgiFeature.getLicense().get());
+        assertEquals("info", osgiFeature.getSCM().get());
+        assertEquals("title", osgiFeature.getName().get());
+        assertEquals("vendor", osgiFeature.getVendor().get());
+        assertTrue(osgiFeature.isComplete());
+        assertEquals(Arrays.asList("c1", "c2"), osgiFeature.getCategories());
+
+        // and back to Sling Feature
+        final Feature slingFeature = Converters.convert(osgiFeature);
+        assertEquals("g:a:1", slingFeature.getId().toMvnId());
+        assertEquals("description", slingFeature.getDescription());
+        assertEquals("doc-url", slingFeature.getDocURL());
+        assertEquals("license", slingFeature.getLicense());
+        assertEquals("info", slingFeature.getSCMInfo());
+        assertEquals("title", slingFeature.getTitle());
+        assertEquals("vendor", slingFeature.getVendor());
+        assertTrue(slingFeature.isComplete());
+        assertEquals(Arrays.asList("c1", "c2"), slingFeature.getCategories());
+    }
+
+    @Test public void testBundleConversion() throws Exception {
+        final Feature feature = new Feature(ArtifactId.parse("g:a:1"));
+        final Artifact bundle = new Artifact(ArtifactId.parse("g:b:2"));
+        bundle.getMetadata().put("key", "foo");
+        feature.getBundles().add(bundle);
+
+        // convert to OSGi feature
+        final org.osgi.service.feature.Feature osgiFeature = Converters.convert(feature);
+        assertEquals(1, osgiFeature.getBundles().size());
+        final org.osgi.service.feature.FeatureBundle ob = osgiFeature.getBundles().get(0);
+        assertEquals("g:b:2", ob.getID().toString());
+        assertEquals(1, ob.getMetadata().size());
+        assertEquals("foo", ob.getMetadata().get("key"));
+
+        // and back to Sling Feature
+        final Feature slingFeature = Converters.convert(osgiFeature);
+        assertEquals(1, slingFeature.getBundles().size());
+        final Artifact sb = slingFeature.getBundles().get(0);
+        assertEquals("g:b:2", sb.getId().toMvnId());
+        assertEquals(1, sb.getMetadata().size());
+        assertEquals("foo", sb.getMetadata().get("key"));
+    }
+
+    @Test public void testConfigurationConversion() throws Exception {
+        final Feature feature = new Feature(ArtifactId.parse("g:a:1"));
+        final Configuration c1 = new Configuration("org.sling.config");
+        c1.getProperties().put("key", "foo");
+        feature.getConfigurations().add(c1);
+        final Configuration c2 = new Configuration("org.sling.factory~name");
+        c2.getProperties().put("value", 5);
+        feature.getConfigurations().add(c2);
+
+        // convert to OSGi feature
+        final org.osgi.service.feature.Feature osgiFeature = Converters.convert(feature);
+        assertEquals(2, osgiFeature.getConfigurations().size());
+        final org.osgi.service.feature.FeatureConfiguration oc1 = osgiFeature.getConfigurations().get("org.sling.config");
+        assertEquals("org.sling.config", oc1.getPid());
+        assertFalse(oc1.getFactoryPid().isPresent());
+        assertEquals(1, oc1.getValues().size());
+        assertEquals("foo", oc1.getValues().get("key"));
+
+        final org.osgi.service.feature.FeatureConfiguration oc2 = osgiFeature.getConfigurations().get("org.sling.factory~name");
+        assertEquals("org.sling.factory~name", oc2.getPid());
+        assertEquals("org.sling.factory", oc2.getFactoryPid().get());
+        assertEquals(1, oc2.getValues().size());
+        assertEquals(5, oc2.getValues().get("value"));
+
+        // and back to Sling Feature
+        final Feature slingFeature = Converters.convert(osgiFeature);
+        assertEquals(2, slingFeature.getConfigurations().size());
+        final Configuration sc1 = slingFeature.getConfigurations().get(0);
+        assertEquals("org.sling.config", sc1.getPid());
+        assertNull(sc1.getFactoryPid());
+        assertNull(sc1.getName());
+        assertEquals(1, sc1.getProperties().size());
+        assertEquals("foo", sc1.getProperties().get("key"));
+
+        final Configuration sc2 = slingFeature.getConfigurations().get(1);
+        assertEquals("org.sling.factory~name", sc2.getPid());
+        assertEquals("org.sling.factory", sc2.getFactoryPid());
+        assertEquals("name", sc2.getName());
+        assertEquals(1, sc2.getProperties().size());
+        assertEquals(5, sc2.getProperties().get("value"));
+    }
+
+    @Test public void testVariablesConversion() throws Exception {
+        final Feature feature = new Feature(ArtifactId.parse("g:a:1"));
+        feature.getVariables().put("v1", "a");
+        feature.getVariableMetadata("v1").put("x", "y");
+
+        // convert to OSGi feature
+        final org.osgi.service.feature.Feature osgiFeature = Converters.convert(feature);
+        assertEquals(1, osgiFeature.getVariables().size());
+        assertEquals("a", osgiFeature.getVariables().get("v1"));
+        assertEquals(1, osgiFeature.getExtensions().size());
+        assertNotNull(osgiFeature.getExtensions().get(Extension.EXTENSION_NAME_INTERNAL_DATA));
+
+        // and back to Sling Feature
+        final Feature slingFeature = Converters.convert(osgiFeature);
+        assertEquals(1, slingFeature.getVariables().size());
+        assertEquals("a", slingFeature.getVariables().get("v1"));
+        assertEquals("y", slingFeature.getVariableMetadata("v1").get("x"));
+        assertTrue(slingFeature.getExtensions().isEmpty());
+    }
+
+    @Test public void testFrameworkPropertiesConversion() throws Exception {
+        final Feature feature = new Feature(ArtifactId.parse("g:a:1"));
+        feature.getFrameworkProperties().put("v1", "a");
+        feature.getFrameworkPropertyMetadata("v1").put("x", "y");
+
+        // convert to OSGi feature
+        final org.osgi.service.feature.Feature osgiFeature = Converters.convert(feature);
+        assertEquals(2, osgiFeature.getExtensions().size());
+        assertNotNull(osgiFeature.getExtensions().get(Extension.EXTENSION_NAME_INTERNAL_DATA));
+        final org.osgi.service.feature.FeatureExtension e = osgiFeature.getExtensions().get("framework-launching-properties");
+        assertNotNull(e);
+        assertEquals(org.osgi.service.feature.FeatureExtension.Type.JSON, e.getType());
+        try ( final StringReader r = new StringReader(e.getJSON()) ) {
+            final Hashtable<String, Object> p = Configurations.buildReader().verifyAsBundleResource(true).build(r).readConfiguration();
+            assertEquals(1, p.size());
+            assertEquals("a", p.get("v1"));
+            }
+
+        // and back to Sling Feature
+        final Feature slingFeature = Converters.convert(osgiFeature);
+        assertEquals(1, slingFeature.getFrameworkProperties().size());
+        assertEquals("a", slingFeature.getFrameworkProperties().get("v1"));
+        assertEquals("y", slingFeature.getFrameworkPropertyMetadata("v1").get("x"));
+        assertTrue(slingFeature.getExtensions().isEmpty());
+    }
+
+    @Test public void testJSONExtensionConversion() throws Exception {
+        final Feature feature = new Feature(ArtifactId.parse("g:a:1"));
+        final Extension e = new Extension(ExtensionType.JSON, "ext", ExtensionState.OPTIONAL);
+        e.setJSON("{\"a\":true}");
+        feature.getExtensions().add(e);
+
+        // convert to OSGi feature
+        final org.osgi.service.feature.Feature osgiFeature = Converters.convert(feature);
+        assertEquals(1, osgiFeature.getExtensions().size());
+        final org.osgi.service.feature.FeatureExtension oe = osgiFeature.getExtensions().get("ext");
+        assertNotNull(oe);
+        assertEquals(org.osgi.service.feature.FeatureExtension.Type.JSON, oe.getType());
+        assertEquals("{\"a\":true}", oe.getJSON());
+
+        // and back to Sling Feature
+        final Feature slingFeature = Converters.convert(osgiFeature);
+        assertEquals(1, slingFeature.getExtensions().size());
+        final Extension se = slingFeature.getExtensions().getByName("ext");
+        assertNotNull(se);
+        assertEquals(ExtensionType.JSON, se.getType());
+        assertEquals("{\"a\":true}", se.getJSON());
+    }
+
+    @Test public void testTextExtensionConversion() throws Exception {
+        final Feature feature = new Feature(ArtifactId.parse("g:a:1"));
+        final Extension e = new Extension(ExtensionType.TEXT, "ext", ExtensionState.OPTIONAL);
+        e.setText("Hello World");
+        feature.getExtensions().add(e);
+
+        // convert to OSGi feature
+        final org.osgi.service.feature.Feature osgiFeature = Converters.convert(feature);
+        assertEquals(1, osgiFeature.getExtensions().size());
+        final org.osgi.service.feature.FeatureExtension oe = osgiFeature.getExtensions().get("ext");
+        assertNotNull(oe);
+        assertEquals(org.osgi.service.feature.FeatureExtension.Type.TEXT, oe.getType());
+        assertEquals("Hello World", String.join("\n", oe.getText()));
+
+        // and back to Sling Feature
+        final Feature slingFeature = Converters.convert(osgiFeature);
+        assertEquals(1, slingFeature.getExtensions().size());
+        final Extension se = slingFeature.getExtensions().getByName("ext");
+        assertNotNull(se);
+        assertEquals(ExtensionType.TEXT, se.getType());
+        assertEquals("Hello World", se.getText());
+    }
+
+    @Test public void testArtifactExtensionConversion() throws Exception {
+        final Feature feature = new Feature(ArtifactId.parse("g:a:1"));
+        final Extension e = new Extension(ExtensionType.ARTIFACTS, "ext", ExtensionState.OPTIONAL);
+        final Artifact artifact = new Artifact(ArtifactId.parse("g:b:2"));
+        artifact.getMetadata().put("key", "foo");
+        e.getArtifacts().add(artifact);
+        feature.getExtensions().add(e);
+
+        // convert to OSGi feature
+        final org.osgi.service.feature.Feature osgiFeature = Converters.convert(feature);
+        assertEquals(1, osgiFeature.getExtensions().size());
+        final org.osgi.service.feature.FeatureExtension oe = osgiFeature.getExtensions().get("ext");
+        assertNotNull(oe);
+        assertEquals(org.osgi.service.feature.FeatureExtension.Type.ARTIFACTS, oe.getType());
+        assertEquals(1, oe.getArtifacts().size());
+        final org.osgi.service.feature.FeatureArtifact oa = oe.getArtifacts().get(0);
+        assertEquals("g:b:2", oa.getID().toString());
+        assertEquals(1, oa.getMetadata().size());
+        assertEquals("foo", oa.getMetadata().get("key"));
+
+        // and back to Sling Feature
+        final Feature slingFeature = Converters.convert(osgiFeature);
+        assertEquals(1, slingFeature.getExtensions().size());
+        final Extension se = slingFeature.getExtensions().getByName("ext");
+        assertNotNull(se);
+        assertEquals(ExtensionType.ARTIFACTS, se.getType());
+        assertEquals(1, se.getArtifacts().size());
+        final Artifact sa = se.getArtifacts().get(0);
+        assertEquals("g:b:2", sa.getId().toMvnId());
+        assertEquals(1, sa.getMetadata().size());
+        assertEquals("foo", sa.getMetadata().get("key"));
+    }
+}