You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by kw...@apache.org on 2021/04/29 13:16:44 UTC

[sling-org-apache-sling-feature-cpconverter] branch master updated: SLING-10243 extract Sling-Initial-Content (#74)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 6b5adbe  SLING-10243 extract Sling-Initial-Content (#74)
6b5adbe is described below

commit 6b5adbebac990c8d2b6f3b43f9bc30b6309484c6
Author: Konrad Windszus <kw...@apache.org>
AuthorDate: Thu Apr 29 15:16:38 2021 +0200

    SLING-10243 extract Sling-Initial-Content (#74)
---
 README.md                                          |  10 +
 pom.xml                                            |  30 +-
 .../ContentPackage2FeatureModelConverter.java      |  81 ++--
 ...ntentPackage2FeatureModelConverterLauncher.java |   6 +-
 .../features/DefaultFeaturesManager.java           |  47 +-
 .../cpconverter/features/FeaturesManager.java      |   4 +
 .../AbstractConfigurationEntryHandler.java         |   6 -
 .../cpconverter/handlers/BundleEntryHandler.java   | 482 +++++++++++++++-----
 .../handlers/DefaultEntryHandlersManager.java      |  16 +-
 .../vltpkg/DefaultPackagesEventsEmitter.java       |  22 +
 .../vltpkg/DocViewSerializerContentHandler.java    | 134 ++++++
 .../DocViewSerializerContentHandlerException.java  |  39 ++
 .../cpconverter/vltpkg/JcrNamespaceRegistry.java   | 103 +++++
 .../cpconverter/vltpkg/SingleFileArchive.java      | 177 ++++++++
 .../feature/cpconverter/vltpkg/ValueConverter.java | 494 +++++++++++++++++++++
 .../cpconverter/vltpkg/VaultPackageAssembler.java  | 121 +++--
 .../cpconverter/vltpkg/VaultPackageUtils.java      |  32 +-
 .../handlers/AbstractBundleEntryHandlerTest.java   |  70 +++
 .../BundleEntryHandleSlingInitialContentTest.java  | 129 ++++++
 .../handlers/BundleEntryHandlerGAVTest.java        |  64 +--
 .../handlers/BundleEntryHandlerTest.java           |  18 +-
 .../handlers/GavDeclarationsInBundleTest.java      |  24 +-
 .../vltpkg/VaultPackageAssemblerTest.java          |   2 +-
 .../handlers/composum-nodes-config-2.5.3.jar       | Bin 0 -> 3748 bytes
 .../handlers/io.wcm.handler.media-1.11.6.jar       | Bin 0 -> 304374 bytes
 25 files changed, 1828 insertions(+), 283 deletions(-)

diff --git a/README.md b/README.md
index fec82c1..b0da791 100644
--- a/README.md
+++ b/README.md
@@ -239,6 +239,11 @@ artifacts/
 12 directories, 8 files
 ```
 
+#### Sling-Initial Content
+
+Bundles containing a [`Sling-Initial-Content` manifest header](https://sling.apache.org/documentation/bundles/content-loading-jcr-contentloader.html) are optionally stripped as well. The contained content in those bundles is converted into content packages or extracted into the feature model. The behaviour of the extraction can be influenced with parameter `--sling-initial-content-policy`.
+
+
 ### OSGi Configurations
 
 All OSGi configuration formats are supported:
@@ -534,6 +539,11 @@ Apache Sling Content Package to Sling Feature converter
       --seed-feature=<seedFeature>
                             A url pointing to a feature that can be assumed to be
                               around when the conversion result will be used
+      --sling-initial-content-policy=<slingInitialContentPolicy>
+                            Determines what to do with Sling-Initial-Content found
+                              in embedded bundles. Valid values: KEEP,
+                              EXTRACT_AND_REMOVE, EXTRACT_AND_KEEP.
+                              Default: KEEP
       --system-user-rel-path=<systemUserRelPath>
                             Relative path for system user as configured with Apache
                               Jackrabbit Oak
diff --git a/pom.xml b/pom.xml
index 10b338a..be3b460 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,7 +35,7 @@
     <sling.java.version>8</sling.java.version>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <picocli.version>3.6.0</picocli.version>
-    <org.apache.jackrabbit.vault.version>3.2.8</org.apache.jackrabbit.vault.version>
+    <org.apache.jackrabbit.vault.version>3.4.10</org.apache.jackrabbit.vault.version>
     <jackrabbit-api.version>2.18.4</jackrabbit-api.version>
     <jackrabbit-spi-commons.version>2.18.4</jackrabbit-spi-commons.version>
     <mockito-core.version>3.2.0</mockito-core.version>
@@ -186,7 +186,33 @@
       <version>${jackrabbit-api.version}</version>
       <scope>provided</scope>
     </dependency>
-
+    <!-- 
+     | Sling-Initial-Content
+     -->
+    <dependency>
+      <groupId>org.apache.sling</groupId>
+      <artifactId>org.apache.sling.jcr.contentloader</artifactId>
+      <version>2.4.1-SNAPSHOT</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sling</groupId>
+      <artifactId>org.apache.sling.commons.osgi</artifactId>
+      <version>2.3.0</version>
+      <scope>provided</scope>
+     </dependency>
+    <dependency>
+      <groupId>org.apache.sling</groupId>
+      <artifactId>org.apache.sling.contentparser.api</artifactId>
+      <version>2.0.0</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sling</groupId>
+      <artifactId>org.apache.sling.contentparser.json</artifactId>
+      <version>2.0.0</version>
+      <scope>provided</scope>
+    </dependency>
     <!--
      | Test only dependencies
     -->
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java b/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
index 49cb21e..3c9f1a4 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
@@ -67,7 +67,7 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
 
     public static final String PACKAGE_CLASSIFIER = "cp2fm-converted";
 
-    private static final String DEFEAULT_VERSION = "0.0.0";
+    private static final String DEFAULT_VERSION = "0.0.0";
 
     private final Map<PackageId, String> subContentPackages = new HashMap<>();
 
@@ -109,6 +109,15 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
     private PackagePolicy contentTypePackagePolicy = PackagePolicy.REFERENCE;
 
     private boolean removeInstallHooks = false;
+    
+    public enum SlingInitialContentPolicy {
+        /** Keep in bundle and don't extract */
+        KEEP,
+        /** Extract from bundle into content-packages and feature model */
+        EXTRACT_AND_REMOVE,
+        /** Extract from bundle into content-packages and feature model but keep in bundle as well */
+        EXTRACT_AND_KEEP
+    }
 
     private final File tmpDirectory;
 
@@ -195,7 +204,7 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
         return this;
     }
 
-    public File getTempDirectory() {
+    public @NotNull File getTempDirectory() {
         return this.tmpDirectory;
     }
     
@@ -259,7 +268,7 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
                 mainPackageAssembler = VaultPackageAssembler.create(this.getTempDirectory(), vaultPackage, removeInstallHooks);
                 assemblers.add(mainPackageAssembler);
 
-                ArtifactId mvnPackageId = toArtifactId(vaultPackage);
+                ArtifactId mvnPackageId = toArtifactId(vaultPackage.getId(), vaultPackage.getFile());
 
                 featuresManager.init(mvnPackageId.getGroupId(), mvnPackageId.getArtifactId(), mvnPackageId.getVersion());
 
@@ -277,7 +286,7 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
 
                 // deploy the new zip content-package to the local mvn bundles dir
 
-                processContentPackageArchive(contentPackageArchive, null, mvnPackageId, vaultPackage.getId());
+                processContentPackageArchive(contentPackageArchive, null);
 
                 // finally serialize the Feature Model(s) file(s)
 
@@ -336,8 +345,6 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
 
         emitters.stream().forEach(e -> e.startSubPackage(path, vaultPackage));
 
-        PackageId originalPackageId = vaultPackage.getId();
-        ArtifactId mvnPackageId = toArtifactId(vaultPackage);
         VaultPackageAssembler clonedPackage = VaultPackageAssembler.create(this.getTempDirectory(), vaultPackage, removeInstallHooks);
 
         // Please note: THIS IS A HACK to meet the new requirement without drastically change the original design
@@ -364,7 +371,7 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
         File contentPackageArchive = clonedPackage.createPackage();
 
         // deploy the new content-package to the local mvn bundles dir and attach it to the feature
-        processContentPackageArchive(contentPackageArchive, runMode, mvnPackageId, originalPackageId);
+        processContentPackageArchive(contentPackageArchive, runMode);
 
         // restore the previous assembler
         mainPackageAssembler = handler;
@@ -372,40 +379,38 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
         emitters.stream().forEach(e -> e.endSubPackage());
     }
 
-    private void processContentPackageArchive(@NotNull File contentPackageArchive,
-                                              @Nullable String runMode,
-                                              @NotNull ArtifactId mvnPackageId,
-                                              @NotNull PackageId originalPackageId) throws Exception {
+    public void processContentPackageArchive(@NotNull File contentPackageArchive,
+                                             @Nullable String runMode) throws Exception {
         try (VaultPackage vaultPackage = open(contentPackageArchive)) {
             PackageType packageType = detectPackageType(vaultPackage);
 
             // SLING-8608 - Fail the conversion if the resulting attached content-package is MIXED type
             if (PackageType.MIXED == packageType && failOnMixedPackages) {
-                throw new Exception("Generated content-package '"
-                                    + originalPackageId
+                throw new IllegalStateException("Generated content-package '"
+                                    + vaultPackage.getId()
                                     + "' located in file "
                                     + contentPackageArchive
                                     + " is of MIXED type");
             }
 
-            
+            ArtifactId mvnPackageId = toArtifactId(vaultPackage.getId(), contentPackageArchive);
             // special handling for converted packages of type content
             if (PackageType.CONTENT == packageType) {
                 switch (contentTypePackagePolicy) {
                     case DROP:
-                        mutableContentsIds.put(originalPackageId, getDependencies(vaultPackage));
+                        mutableContentsIds.put(vaultPackage.getId(), getDependencies(vaultPackage));
                         logger.info("Dropping package of PackageType.CONTENT {} (content-package id: {})",
-                                    mvnPackageId.getArtifactId(), originalPackageId);
+                                    mvnPackageId.getArtifactId(), vaultPackage.getId());
                         break;
                     case PUT_IN_DEDICATED_FOLDER:
-                        mutableContentsIds.put(originalPackageId, getDependencies(vaultPackage));
+                        mutableContentsIds.put(vaultPackage.getId(), getDependencies(vaultPackage));
                         // deploy the new content-package to the unreferenced artifacts deployer
                         if (unreferencedArtifactsDeployer == null) {
                             throw new IllegalStateException("ContentTypePackagePolicy PUT_IN_DEDICATED_FOLDER requires a valid deployer ");
                         }
                         unreferencedArtifactsDeployer.deploy(new FileArtifactWriter(contentPackageArchive), mvnPackageId);
                         logger.info("Put converted package of PackageType.CONTENT {} (content-package id: {}) in {} (not referenced in feature model)",
-                                    mvnPackageId.getArtifactId(), originalPackageId, unreferencedArtifactsDeployer.getBaseDirectory());
+                                    mvnPackageId.getArtifactId(), vaultPackage.getId(), unreferencedArtifactsDeployer.getBaseDirectory());
                         break;
                     case REFERENCE:
                         artifactsDeployer.deploy(new FileArtifactWriter(contentPackageArchive), mvnPackageId);
@@ -413,8 +418,8 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
                 }
             } else {
                 // deploy the new content-package to the local mvn bundles dir
-                artifactsDeployer.deploy(new FileArtifactWriter(contentPackageArchive), mvnPackageId);
-                featuresManager.addArtifact(runMode, mvnPackageId);
+                getArtifactsDeployer().deploy(new FileArtifactWriter(contentPackageArchive), mvnPackageId);
+                getFeaturesManager().addArtifact(runMode, mvnPackageId);
             }
         }
     }
@@ -423,30 +428,44 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
         return subContentPackages.containsValue(path);
     }
 
-    @Override
-    protected void onFile(@NotNull String entryPath, @NotNull Archive archive, @NotNull Entry entry) throws Exception {
+    public boolean process(@NotNull String entryPath, @NotNull Archive archive, @Nullable Entry entry, boolean useMainPackageAssembler) throws Exception {
         if (resourceFilter != null && resourceFilter.isFilteredOut(entryPath)) {
             throw new IllegalArgumentException("Path '"
                                                + entryPath
                                                + "' in archive "
-                                               + archive.getMetaInf().getProperties()
+                                               + archive.getMetaInf().getPackageProperties().getId()
                                                + " not allowed by user configuration, please check configured filtering patterns");
         }
 
         EntryHandler entryHandler = handlersManager.getEntryHandlerByEntryPath(entryPath);
         if (entryHandler == null) {
-            entryHandler = mainPackageAssembler;
+            if (useMainPackageAssembler) {
+                entryHandler = mainPackageAssembler;
+            } else {
+                return false;
+            }
         }
 
+        if (entry == null) {
+            entry = archive.getEntry(entryPath);
+            if (entry == null) {
+                throw new IllegalArgumentException("Archive '" + archive.getMetaInf().getPackageProperties().getId() + "' does not contain entry with path '" + entryPath + "'");
+            }
+        }
         entryHandler.handle(entryPath, archive, entry, this);
+        return true;
+    }
+
+    @Override
+    protected void onFile(@NotNull String entryPath, @NotNull Archive archive, @NotNull Entry entry) throws Exception {
+        process(entryPath, archive, entry, true);
     }
 
-    private static @NotNull ArtifactId toArtifactId(@NotNull VaultPackage vaultPackage) {
-        PackageId packageId = vaultPackage.getId();
+    public static @NotNull ArtifactId toArtifactId(@NotNull PackageId packageId, @NotNull File file) {
         String groupId = requireNonNull(packageId.getGroup(),
             PackageProperties.NAME_GROUP
                 + " property not found in content-package "
-                + vaultPackage
+                + file
                 + ", please check META-INF/vault/properties.xml").replace('/', '.');
         // Replace any space with an underscore to adhere to Maven Group Id specification
         groupId = groupId.replaceAll(" ", "_");
@@ -454,14 +473,18 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
         String artifactid = requireNonNull(packageId.getName(),
             PackageProperties.NAME_NAME
                 + " property not found in content-package "
-                + vaultPackage
+                + file
                 + ", please check META-INF/vault/properties.xml");
         // Replace any space with an underscore to adhere to Maven Artifact Id specification
         artifactid = artifactid.replaceAll(" ", "_");
 
+        // package versions may use suffix "-cp2fm-converted" which is redundant as for artifactIds this is set as dedicated classifier
         String version = packageId.getVersionString();
+        if (version.endsWith(VaultPackageAssembler.VERSION_SUFFIX)) {
+            version = version.substring(0, version.length() - VaultPackageAssembler.VERSION_SUFFIX.length());
+        }
         if (version == null || version.isEmpty()) {
-            version = DEFEAULT_VERSION;
+            version = DEFAULT_VERSION;
         }
 
         return new ArtifactId(groupId, artifactid, version, PACKAGE_CLASSIFIER, ZIP_TYPE);
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/cli/ContentPackage2FeatureModelConverterLauncher.java b/src/main/java/org/apache/sling/feature/cpconverter/cli/ContentPackage2FeatureModelConverterLauncher.java
index 4072487..c18953e 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/cli/ContentPackage2FeatureModelConverterLauncher.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/cli/ContentPackage2FeatureModelConverterLauncher.java
@@ -29,6 +29,7 @@ import java.util.TimeZone;
 
 import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
+import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter.SlingInitialContentPolicy;
 import org.apache.sling.feature.cpconverter.accesscontrol.AclManager;
 import org.apache.sling.feature.cpconverter.accesscontrol.DefaultAclManager;
 import org.apache.sling.feature.cpconverter.artifacts.LocalMavenRepositoryArtifactsDeployer;
@@ -133,6 +134,9 @@ public final class ContentPackage2FeatureModelConverterLauncher implements Runna
     @Option(names = { "-u", "--unreferenced-artifacts-output-directory" }, description = "The output directory where unreferenced artifacts will be deployed.", required = false)
     private File unreferencedArtifactsOutputDirectory;
 
+    @Option(names = { "--sling-initial-content-policy" }, description = "Determines what to do with Sling-Initial-Content found in embedded bundles. Valid values: ${COMPLETION-CANDIDATES}.", required = false, showDefaultValue = Visibility.ALWAYS)
+    private SlingInitialContentPolicy slingInitialContentPolicy = SlingInitialContentPolicy.KEEP;
+
     @Override
     public void run() {
         if (quiet) {
@@ -204,7 +208,7 @@ public final class ContentPackage2FeatureModelConverterLauncher implements Runna
                 ContentPackage2FeatureModelConverter converter = new ContentPackage2FeatureModelConverter(strictValidation)
                                                                 .setFeaturesManager(featuresManager)
                                                                 .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(artifactsOutputDirectory))
-                                                                .setEntryHandlersManager(new DefaultEntryHandlersManager(entryHandlerConfigsMap, !disableInstallerPolicy))
+                                                                .setEntryHandlersManager(new DefaultEntryHandlersManager(entryHandlerConfigsMap, !disableInstallerPolicy, slingInitialContentPolicy))
                                                                 .setAclManager(aclManager)
                                                                 .setEmitter(DefaultPackagesEventsEmitter.open(featureModelsOutputDirectory))
                                                                 .setFailOnMixedPackages(failOnMixedPackages)
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java b/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java
index b16425c..c75c7f8 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java
@@ -22,8 +22,19 @@ import static org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelCo
 import java.io.File;
 import java.io.FileWriter;
 import java.io.IOException;
-import java.util.*;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Stack;
 
 import org.apache.jackrabbit.vault.packaging.VaultPackage;
 import org.apache.sling.feature.Artifact;
@@ -39,11 +50,16 @@ import org.apache.sling.feature.cpconverter.accesscontrol.AclManager;
 import org.apache.sling.feature.cpconverter.accesscontrol.Mapping;
 import org.apache.sling.feature.cpconverter.interpolator.SimpleVariablesInterpolator;
 import org.apache.sling.feature.cpconverter.interpolator.VariablesInterpolator;
+import org.apache.sling.feature.cpconverter.repoinit.NoOpVisitor;
 import org.apache.sling.feature.cpconverter.vltpkg.PackagesEventsEmitter;
 import org.apache.sling.feature.extension.apiregions.api.ApiExport;
 import org.apache.sling.feature.extension.apiregions.api.ApiRegion;
 import org.apache.sling.feature.extension.apiregions.api.ApiRegions;
 import org.apache.sling.feature.io.json.FeatureJSONWriter;
+import org.apache.sling.repoinit.parser.RepoInitParsingException;
+import org.apache.sling.repoinit.parser.impl.RepoInitParserService;
+import org.apache.sling.repoinit.parser.operations.Operation;
+import org.apache.sling.repoinit.parser.operations.RegisterNamespace;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 import org.osgi.framework.Constants;
@@ -89,6 +105,8 @@ public class DefaultFeaturesManager implements FeaturesManager, PackagesEventsEm
     private final Map<String, String> properties;
 
     private final List<String> targetAPIRegions = new ArrayList<>();
+    
+    private final Map<String, String> namespaceUriByPrefix;
 
     private String exportsToAPIRegion;
 
@@ -133,6 +151,7 @@ public class DefaultFeaturesManager implements FeaturesManager, PackagesEventsEm
         this.prefix = prefix;
         this.properties = properties;
         this.aclManager = aclManager;
+        this.namespaceUriByPrefix = new HashMap<>();
     }
 
     @Override
@@ -252,10 +271,34 @@ public class DefaultFeaturesManager implements FeaturesManager, PackagesEventsEm
             handleRepoinitAndMappings("seed", conf.isFactoryConfiguration() ? conf.getFactoryPid() : conf.getPid(), conf.getConfigurationProperties(), false);
         }
         if (seed.getExtensions().getByName(Extension.EXTENSION_NAME_REPOINIT) != null) {
-            getAclManager().addRepoinitExtention(seed.getExtensions().getByName(Extension.EXTENSION_NAME_REPOINIT).getText(), "seed", this);
+            String repoInitText = seed.getExtensions().getByName(Extension.EXTENSION_NAME_REPOINIT).getText();
+            getAclManager().addRepoinitExtention(repoInitText, "seed", this);
+            extractNamespaces(repoInitText, namespaceUriByPrefix);
+        }
+        
+    }
+
+    static void extractNamespaces(String repoInitText, Map<String, String> namespaceUriByPrefix) {
+        try {
+            List<Operation> ops = new RepoInitParserService().parse(new StringReader(repoInitText));
+            for (Operation op : ops) {
+                op.accept(new NoOpVisitor() {
+                    @Override
+                    public void visitRegisterNamespace(RegisterNamespace registerNamespace) {
+                        namespaceUriByPrefix.put(registerNamespace.getPrefix(), registerNamespace.getURI());
+                    }
+                });
+            }
+        } catch (RepoInitParsingException e) {
+            throw new IllegalArgumentException(e);
         }
     }
 
+    @Override
+    public@ NotNull Map<String, String> getNamespaceUriByPrefix() {
+        return namespaceUriByPrefix;
+    }
+
     @NotNull AclManager getAclManager() {
         return Objects.requireNonNull(this.aclManager);
     }
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/features/FeaturesManager.java b/src/main/java/org/apache/sling/feature/cpconverter/features/FeaturesManager.java
index 32b21c2..b9ac8f4 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/features/FeaturesManager.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/features/FeaturesManager.java
@@ -17,6 +17,7 @@
 package org.apache.sling.feature.cpconverter.features;
 
 import java.util.Dictionary;
+import java.util.Map;
 
 import org.apache.sling.feature.ArtifactId;
 import org.apache.sling.feature.Feature;
@@ -46,4 +47,7 @@ public interface FeaturesManager {
 
     void addOrAppendRepoInitExtension(@NotNull String text, @Nullable String runMode);
 
+    @NotNull
+    Map<String, String> getNamespaceUriByPrefix();
+
 }
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/handlers/AbstractConfigurationEntryHandler.java b/src/main/java/org/apache/sling/feature/cpconverter/handlers/AbstractConfigurationEntryHandler.java
index 4e68ca2..bb84b81 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/handlers/AbstractConfigurationEntryHandler.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/handlers/AbstractConfigurationEntryHandler.java
@@ -17,22 +17,16 @@
 package org.apache.sling.feature.cpconverter.handlers;
 
 import java.io.InputStream;
-import java.util.ArrayList;
 import java.util.Dictionary;
-import java.util.List;
 import java.util.Objects;
 import java.util.regex.Matcher;
 
 import org.apache.jackrabbit.vault.fs.io.Archive;
 import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
 import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
-import org.apache.sling.feature.cpconverter.accesscontrol.AclManager;
-import org.apache.sling.feature.cpconverter.accesscontrol.Mapping;
 import org.apache.sling.feature.cpconverter.features.FeaturesManager;
-import org.apache.sling.feature.cpconverter.repoinit.OperationProcessor;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
-import org.osgi.util.converter.Converters;
 
 abstract class AbstractConfigurationEntryHandler extends AbstractRegexEntryHandler {
 
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandler.java b/src/main/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandler.java
index fa48113..0075671 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandler.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandler.java
@@ -16,30 +16,76 @@
  */
 package org.apache.sling.feature.cpconverter.handlers;
 
-import org.apache.felix.utils.manifest.Clause;
-import org.apache.felix.utils.manifest.Parser;
-import org.apache.jackrabbit.vault.fs.io.Archive;
-import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
-import org.apache.sling.feature.ArtifactId;
-import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
-import org.apache.sling.feature.cpconverter.artifacts.InputStreamArtifactWriter;
-import org.jetbrains.annotations.NotNull;
-import org.osgi.framework.Constants;
-import org.osgi.framework.Version;
+import static java.util.Objects.requireNonNull;
+import static org.osgi.framework.Version.parseVersion;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Properties;
+import java.util.StringTokenizer;
+import java.util.jar.Attributes;
 import java.util.jar.JarEntry;
-import java.util.jar.JarInputStream;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
 import java.util.jar.Manifest;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import static java.util.Objects.requireNonNull;
-import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_VERSION;
-import static org.osgi.framework.Version.parseVersion;
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.felix.utils.manifest.Clause;
+import org.apache.felix.utils.manifest.Parser;
+import org.apache.jackrabbit.commons.cnd.ParseException;
+import org.apache.jackrabbit.util.Text;
+import org.apache.jackrabbit.vault.fs.api.ImportMode;
+import org.apache.jackrabbit.vault.fs.api.PathFilterSet;
+import org.apache.jackrabbit.vault.fs.config.DefaultWorkspaceFilter;
+import org.apache.jackrabbit.vault.fs.io.Archive;
+import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
+import org.apache.jackrabbit.vault.packaging.PackageId;
+import org.apache.jackrabbit.vault.packaging.PackageProperties;
+import org.apache.jackrabbit.vault.packaging.PackageType;
+import org.apache.jackrabbit.vault.util.PlatformNameFormat;
+import org.apache.sling.commons.osgi.ManifestHeader;
+import org.apache.sling.contentparser.api.ContentParser;
+import org.apache.sling.contentparser.api.ParserOptions;
+import org.apache.sling.contentparser.json.JSONParserOptions;
+import org.apache.sling.contentparser.json.internal.JSONContentParser;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
+import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter.SlingInitialContentPolicy;
+import org.apache.sling.feature.cpconverter.artifacts.InputStreamArtifactWriter;
+import org.apache.sling.feature.cpconverter.vltpkg.DocViewSerializerContentHandler;
+import org.apache.sling.feature.cpconverter.vltpkg.DocViewSerializerContentHandlerException;
+import org.apache.sling.feature.cpconverter.vltpkg.JcrNamespaceRegistry;
+import org.apache.sling.feature.cpconverter.vltpkg.SingleFileArchive;
+import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageAssembler;
+import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageUtils;
+import org.apache.sling.jcr.contentloader.PathEntry;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
 
 public final class BundleEntryHandler extends AbstractRegexEntryHandler {
 
@@ -47,20 +93,20 @@ public final class BundleEntryHandler extends AbstractRegexEntryHandler {
 
     private static final String NAME_ARTIFACT_ID = "artifactId";
 
-    private static final String NAME_CLASSIFIER = "classifier";
+    private static final String JAR_TYPE = "jar";
 
-    private static final String BUNDLE_SYMBOLIC_NAME = "Bundle-SymbolicName";
+    public static final String NODETYPES_BUNDLE_HEADER = "Sling-Nodetypes";
 
-    private static final String BUNDLE_VERSION = "Bundle-Version";
+    public static final String NAMESPACES_BUNDLE_HEADER = "Sling-Namespaces";
 
-    private static final String JAR_TYPE = "jar";
+    private static final Pattern POM_PROPERTIES_PATTERN = Pattern.compile("META-INF/maven/[^/]+/[^/]+/pom.properties");
 
-    private final Pattern pomPropertiesPattern = Pattern.compile("META-INF/maven/[^/]+/[^/]+/pom.properties");
-
-    private final Pattern pomXmlPropertiesPattern = Pattern.compile("META-INF/maven/[^/]+/[^/]+/pom.xml");
+    private static final Pattern POM_XML_PATTERN = Pattern.compile("META-INF/maven/[^/]+/[^/]+/pom.xml");
 
     private boolean enforceBundlesBelowInstallFolder;
 
+    private SlingInitialContentPolicy slingInitialContentPolicy;
+
     public BundleEntryHandler() {
         super("/jcr_root/(?:apps|libs)/.+/(?<foldername>install|config)(?:\\.(?<runmode>[^/]+))?/(?:(?<startlevel>[0-9]+)/)?.+\\.jar");
     }
@@ -69,16 +115,14 @@ public final class BundleEntryHandler extends AbstractRegexEntryHandler {
         this.enforceBundlesBelowInstallFolder = enforceBundlesBelowInstallFolder;
     }
 
+    void setSlingInitialContentPolicy(SlingInitialContentPolicy slingInitialContentPolicy) {
+        this.slingInitialContentPolicy = slingInitialContentPolicy;
+    }
+
     @Override
     public void handle(@NotNull String path, @NotNull Archive archive, @NotNull Entry entry, @NotNull ContentPackage2FeatureModelConverter converter) throws Exception {
         logger.info("Processing bundle {}...", entry.getName());
 
-        String groupId;
-        String artifactId = null;
-        String version;
-        String classifier = null;
-        Manifest manifest;
-
         Matcher matcher = getPattern().matcher(path);
         String runMode = null;
         Integer startLevel = null;
@@ -109,111 +153,323 @@ public final class BundleEntryHandler extends AbstractRegexEntryHandler {
             logger.debug("Start level {} was extracted from path {}", startLevel, path);
         }
 
-        try (JarInputStream jarInput = new JarInputStream(Objects.requireNonNull(archive.openInputStream(entry)))) {
-            Properties properties = readGav(entry.getName(), jarInput);
-            manifest = jarInput.getManifest();
-
-            if (!properties.isEmpty()) {
-                groupId = getCheckedProperty(properties, NAME_GROUP_ID);
-                artifactId = getCheckedProperty(properties, NAME_ARTIFACT_ID);
-                version = getCheckedProperty(properties, NAME_VERSION);
-                classifier = properties.getProperty(NAME_CLASSIFIER);
-            } else { // maybe the included jar is just an OSGi bundle but not a valid Maven artifact
-                groupId = getCheckedProperty(manifest, BUNDLE_SYMBOLIC_NAME);
-                // Make sure there are not spaces in the name to adhere to the Maven Group Id specification
-                groupId = groupId.replace(' ', '_').replace(':', '_').replace('/', '_').replace('\\', '_');
-                if (groupId.indexOf('.') != -1) {
-                    artifactId = groupId.substring(groupId.lastIndexOf('.') + 1);
-                    groupId = groupId.substring(0, groupId.lastIndexOf('.'));
+        String bundleName = entry.getName();
+        // Remove the leading path
+        int idx = bundleName.lastIndexOf('/');
+        if (idx >= 0) {
+            bundleName = bundleName.substring(idx + 1);
+        }
+        // Remove the extension
+        int edx = bundleName.lastIndexOf('.');
+        if (edx > 0) {
+            bundleName = bundleName.substring(0, edx);
+        }
+        
+        // create a temporary JAR file (extracted from archive)
+        Path tmpBundleJar = Files.createTempFile(converter.getTempDirectory().toPath(), "extracted", bundleName + ".jar");
+        try (OutputStream output = Files.newOutputStream(tmpBundleJar);
+             InputStream input = Objects.requireNonNull(archive.openInputStream(entry))) {
+            IOUtils.copy(input, output);
+        }
+        try {
+            processBundleInputStream(tmpBundleJar, bundleName, runMode, startLevel, converter);
+        } finally {
+            Files.delete(tmpBundleJar);
+        }
+    }
+
+    void processBundleInputStream(@NotNull Path originalBundleFile, @NotNull String bundleName, @Nullable String runMode, @Nullable Integer startLevel, @NotNull ContentPackage2FeatureModelConverter converter) throws Exception {
+        try (JarFile jarFile = new JarFile(originalBundleFile.toFile())) {
+            // first extract bundle metadata from JAR input stream
+            ArtifactId id = extractArtifactId(bundleName, jarFile);
+
+            try (InputStream strippedBundleInput = extractSlingInitialContent(id, jarFile, converter, runMode)) {
+                if (strippedBundleInput != null && slingInitialContentPolicy == SlingInitialContentPolicy.EXTRACT_AND_REMOVE) {
+                    id = id.changeVersion(id.getVersion() + "-" + ContentPackage2FeatureModelConverter.PACKAGE_CLASSIFIER);
+                    Objects.requireNonNull(converter.getArtifactsDeployer()).deploy(new InputStreamArtifactWriter(strippedBundleInput), id);
+                } else {
+                    try (InputStream originalBundleInput = Files.newInputStream(originalBundleFile)) {
+                        Objects.requireNonNull(converter.getArtifactsDeployer()).deploy(new InputStreamArtifactWriter(originalBundleInput), id);
+                    }
                 }
-                if (artifactId == null || artifactId.isEmpty()) {
-                    artifactId = groupId;
+            }
+            Objects.requireNonNull(converter.getFeaturesManager()).addArtifact(runMode, id, startLevel);
+            String exportHeader = Objects.requireNonNull(jarFile.getManifest()).getMainAttributes().getValue(Constants.EXPORT_PACKAGE);
+            if (exportHeader != null) {
+                for (Clause clause : Parser.parseHeader(exportHeader)) {
+                    converter.getFeaturesManager().addAPIRegionExport(runMode, clause.getName());
                 }
-                Version osgiVersion = Version.parseVersion(getCheckedProperty(manifest, BUNDLE_VERSION));
-                version = osgiVersion.getMajor() + "." + osgiVersion.getMinor() + "." + osgiVersion.getMicro() + (osgiVersion.getQualifier().isEmpty() ? "" : "-" + osgiVersion.getQualifier());
             }
         }
+    }
 
-        try (InputStream input = archive.openInputStream(entry)) {
-            if (input != null) {
-                ArtifactId id = new ArtifactId(groupId, artifactId, version, classifier, JAR_TYPE);
+    static Version getModifiedOsgiVersion(Version originalVersion) {
+        return new Version(originalVersion.getMajor(), originalVersion.getMinor(), originalVersion.getMicro(), originalVersion.getQualifier() + "_" + ContentPackage2FeatureModelConverter.PACKAGE_CLASSIFIER);
+    }
 
-                Objects.requireNonNull(converter.getArtifactsDeployer()).deploy(new InputStreamArtifactWriter(input), id);
+    @Nullable InputStream extractSlingInitialContent(@NotNull ArtifactId bundleArtifactId, @NotNull JarFile jarFile, @NotNull ContentPackage2FeatureModelConverter converter, @Nullable String runMode) throws Exception {
+        if (slingInitialContentPolicy == SlingInitialContentPolicy.KEEP) {
+            return null;
+        }
+        // parse "Sling-Initial-Content" header
+        Manifest manifest = Objects.requireNonNull(jarFile.getManifest());
+        Iterator<PathEntry> pathEntries = PathEntry.getContentPaths(manifest, -1);
+        if (pathEntries == null) {
+            return null;
+        }
+        logger.info("Extracting Sling-Initial-Content from '{}'", bundleArtifactId);
+        Collection<PathEntry> pathEntryList = new ArrayList<>();
+        pathEntries.forEachRemaining(pathEntryList::add);
+
+        // remove header
+        manifest.getMainAttributes().remove(new Attributes.Name(PathEntry.CONTENT_HEADER));
+        // change version to have suffix
+        Version originalVersion = new Version(Objects.requireNonNull(manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION)));
+        manifest.getMainAttributes().putValue(Constants.BUNDLE_VERSION, getModifiedOsgiVersion(originalVersion).toString());
+        Path newBundleFile = Files.createTempFile(converter.getTempDirectory().toPath(), "newBundle", ".jar");
+        
+        // create JAR file to prevent extracting it twice and for random access
+        JcrNamespaceRegistry namespaceRegistry = createNamespaceRegistry(manifest, jarFile, converter.getFeaturesManager().getNamespaceUriByPrefix());
+        
+        Map<PackageType, VaultPackageAssembler> packageAssemblers = new EnumMap<>(PackageType.class);
+        try (OutputStream fileOutput = Files.newOutputStream(newBundleFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
+            JarOutputStream bundleOutput = new JarOutputStream(fileOutput, manifest)) {
+            
+            for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements();) {
+                JarEntry jarEntry = e.nextElement();
+                if (!jarEntry.isDirectory()) {
+                    try (InputStream input = jarFile.getInputStream(jarEntry)) {
+                        if (!extractSlingInitialContent(jarEntry, input, bundleArtifactId, pathEntryList, packageAssemblers, namespaceRegistry, converter)) {
+                            // skip manifest, as already written in the constructor (as first entry)
+                            if (jarEntry.getName().equals(JarFile.MANIFEST_NAME)) {
+                                continue;
+                            }
+                            // copy entry as is to the stripped bundle
+                            bundleOutput.putNextEntry(jarEntry);
+                            IOUtils.copy(input, bundleOutput);
+                            bundleOutput.closeEntry();
+                        }
+                    }
+                }
+            }
+        }
+        // add additional content packages to feature model
+        finalizePackageAssembly(packageAssemblers, converter, runMode);
+        
+        // return stripped bundle's inputstream which must be deleted on close
+        return Files.newInputStream(newBundleFile, StandardOpenOption.READ, StandardOpenOption.DELETE_ON_CLOSE);
+    }
 
-                Objects.requireNonNull(converter.getFeaturesManager()).addArtifact(runMode, id, startLevel);
+    /**
+     * 
+     * @param jarEntry
+     * @param bundleFileInputStream
+     * @param pathEntriesStream
+     * @param packageAssemblers
+     * @param converter
+     * @return {@code true} in case the given entry was part of the initial content otherwise {@code false}
+     * @throws Exception 
+     */
+    boolean extractSlingInitialContent(@NotNull JarEntry jarEntry, @NotNull InputStream bundleFileInputStream, @NotNull ArtifactId bundleArtifactId, @NotNull Collection<PathEntry> pathEntries, @NotNull Map<PackageType, VaultPackageAssembler> packageAssemblers, @NotNull JcrNamespaceRegistry nsRegistry, @NotNull ContentPackage2FeatureModelConverter converter) throws Exception {
+        final String entryName = jarEntry.getName();
+        // check if current JAR entry is initial content
+        Optional<PathEntry> pathEntry = pathEntries.stream().filter(p -> entryName.startsWith(p.getPath())).findFirst();
+        if (!pathEntry.isPresent()) {
+            return false;
+        }
+        Map.Entry<ContentParser, ParserOptions> contentParserAndOptions = getContentParserForEntry(jarEntry, pathEntry.get());
+
+        // https://sling.apache.org/documentation/bundles/content-loading-jcr-contentloader.html#file-name-escaping
+        String repositoryPath = (pathEntry.get().getTarget() != null ? pathEntry.get().getTarget() : "/") + URLDecoder.decode(entryName.substring(pathEntry.get().getPath().length()), "UTF-8");
+        // all entry paths used by entry handlers start with "/"
+        String contentPackageEntryPath = "/" + org.apache.jackrabbit.vault.util.Constants.ROOT_DIR + PlatformNameFormat.getPlatformPath(repositoryPath);
+
+        Path tmpDocViewInputFile = null;
+        try {
+            if (contentParserAndOptions != null) {
+                // convert to docview xml
+                tmpDocViewInputFile = Files.createTempFile(converter.getTempDirectory().toPath(), "docview", ".xml");
+                try (OutputStream docViewOutput = Files.newOutputStream(tmpDocViewInputFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING);
+                     DocViewSerializerContentHandler contentHandler = new DocViewSerializerContentHandler(docViewOutput, nsRegistry)) {
+                    contentParserAndOptions.getKey().parse(contentHandler, bundleFileInputStream, contentParserAndOptions.getValue());
+                    contentPackageEntryPath = FilenameUtils.removeExtension(contentPackageEntryPath) + ".xml";
+                } catch (IOException e) {
+                    throw new IOException("Can not parse " + jarEntry, e);
+                } catch (DocViewSerializerContentHandlerException e) {
+                    throw new IOException("Can not convert " + jarEntry + " to enhanced DocView format", e);
+                }
+            }
+    
+            // remap CND files to make sure they are picked up by NodeTypesEntryHandler
+            if (nsRegistry.getRegisteredCndSystemIds().contains(jarEntry.getName())) {
+                contentPackageEntryPath = "/META-INF/vault/" + Text.getName(jarEntry.getName()) + ".cnd";
+            }
+            try (Archive virtualArchive = SingleFileArchive.fromPathOrInputStream(tmpDocViewInputFile, bundleFileInputStream, 
+                    () -> Files.createTempFile(converter.getTempDirectory().toPath(), "initial-content", Text.getName(jarEntry.getName())), contentPackageEntryPath)) {
+                // does entry in initial content need to be extracted into feature model (e.g. for OSGi configurations)?
+                if (!converter.process(contentPackageEntryPath, virtualArchive, null, false)) {
+                    // ... otherwise add it to the content package
+                    // in which content package should this end up?
+                    VaultPackageAssembler packageAssembler = initPackageAssemblerForPath(bundleArtifactId, repositoryPath, pathEntry.get(), packageAssemblers, converter);
+                    if (tmpDocViewInputFile != null) {
+                        packageAssembler.addEntry(contentPackageEntryPath, tmpDocViewInputFile.toFile());
+                    } else {
+                        packageAssembler.addEntry(contentPackageEntryPath, bundleFileInputStream);
+                    }
+                }
+            }
+        } finally {
+            if (tmpDocViewInputFile != null) {
+                Files.delete(tmpDocViewInputFile);
+            }
+        }
+        return true;
+    }
+
+    JcrNamespaceRegistry createNamespaceRegistry(@NotNull Manifest manifest, @NotNull JarFile jarFile, @NotNull Map<String, String> predefinedNamespaceUriByPrefix) throws RepositoryException, IOException, ParseException {
+        JcrNamespaceRegistry registry = new JcrNamespaceRegistry();
+        for (Map.Entry<String, String> entry : predefinedNamespaceUriByPrefix.entrySet()) {
+            registry.registerNamespace(entry.getKey(), entry.getValue());
+        }
+
+        // parse Sling-Namespaces header (https://github.com/apache/sling-org-apache-sling-jcr-base/blob/66be360910c265473799635fcac0e23895898913/src/main/java/org/apache/sling/jcr/base/internal/loader/Loader.java#L192)
+        final String namespacesDefinitionHeader = manifest.getMainAttributes().getValue(NAMESPACES_BUNDLE_HEADER);
+        if (namespacesDefinitionHeader != null) {
+            final StringTokenizer st = new StringTokenizer(namespacesDefinitionHeader, ",");
+
+            while ( st.hasMoreTokens() ) {
+                final String token = st.nextToken().trim();
+                int pos = token.indexOf('=');
+                if ( pos == -1 ) {
+                    logger.warn("createNamespaceRegistry: Bundle {} has an invalid namespace manifest header entry: {}",
+                            manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME), token);
+                } else {
+                    final String prefix = token.substring(0, pos).trim();
+                    final String namespace = token.substring(pos+1).trim();
+                    registry.registerNamespace(prefix, namespace);
+                }
+            }
+        }
 
-                String epHeader = manifest.getMainAttributes().getValue(Constants.EXPORT_PACKAGE);
-                if (epHeader != null) {
-                    for (Clause clause : Parser.parseHeader(epHeader)) {
-                        converter.getFeaturesManager().addAPIRegionExport(runMode, clause.getName());
+        // parse Sling-Nodetypes header
+        final String typesHeader = manifest.getMainAttributes().getValue(NODETYPES_BUNDLE_HEADER);
+        if (typesHeader != null) {
+            for (ManifestHeader.Entry entry : ManifestHeader.parse(typesHeader).getEntries()) {
+                JarEntry jarEntry = jarFile.getJarEntry(entry.getValue());
+                if (jarEntry == null) {
+                    logger.warn("createNamespaceRegistry: Bundle {} has referenced a non existing node type definition: {}",
+                            manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME), entry.getValue());
+                } else {
+                    try (InputStream inputStream = jarFile.getInputStream(jarEntry);
+                         Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
+                        registry.registerCnd(reader, entry.getValue());
                     }
                 }
             }
         }
+        return registry;
     }
 
-    // method visibility set to 'protected' fot testing purposes
-    protected @NotNull Properties readGav(@NotNull String entryName, @NotNull JarInputStream jarInput) throws IOException {
-        Properties pomProperties = new Properties();
-        Properties pomXmlProperties = new Properties();
+    /**
+     * Lazily initializes the cache with the necessary VaultPackageAssemblers
+     * @param bundleArtifactId
+     * @param repositoryPath
+     * @param cache
+     * @param converter
+     * @return the VaultPackageAssembler from the cache to use for the given repository path
+     */
+    public VaultPackageAssembler initPackageAssemblerForPath(@NotNull ArtifactId bundleArtifactId, @NotNull String repositoryPath, @NotNull PathEntry pathEntry, @NotNull Map<PackageType, VaultPackageAssembler> cache, @NotNull ContentPackage2FeatureModelConverter converter) {
+        PackageType packageType = VaultPackageUtils.detectPackageType(repositoryPath);
+        VaultPackageAssembler assembler = cache.get(packageType);
+        if (assembler == null) {
+            final String packageNameSuffix;
+            switch (packageType) {
+                case APPLICATION:
+                    packageNameSuffix = "-apps";
+                    break;
+                case CONTENT:
+                    packageNameSuffix = "-content";
+                    break;
+                default:
+                    throw new IllegalStateException("Unexpected package type " + packageType + " detected for path " + repositoryPath);
+            }
+            final PackageId packageId = new PackageId(bundleArtifactId.getGroupId(), bundleArtifactId.getArtifactId()+packageNameSuffix, bundleArtifactId.getVersion());
+            assembler = VaultPackageAssembler.create(converter.getTempDirectory(), packageId, "Generated out of Sling Initial Content from bundle " + bundleArtifactId + " by cp2fm");
+            cache.put(packageType, assembler);
+            logger.info("Created package {} out of Sling-Initial-Content from '{}'", packageId, bundleArtifactId);
+        }
+        DefaultWorkspaceFilter filter = assembler.getFilter();
+        if (!filter.covers(repositoryPath)) {
+            PathFilterSet pathFilterSet = new PathFilterSet(pathEntry.getTarget() != null ? pathEntry.getTarget() : "/");
+            ImportMode importMode;
+            if (pathEntry.isOverwrite()) {
+                importMode = ImportMode.REPLACE;
+            } else {
+                importMode = ImportMode.MERGE;
+            }
+            // TODO: add handling for merge, mergeProperties and overwriteProperties (https://issues.apache.org/jira/browse/SLING-10318)
+            pathFilterSet.setImportMode(importMode);
+            filter.add(pathFilterSet);
+        }
+        return assembler;
+    }
 
-        String bundleName = entryName;
-        // Remove the leading path
-        int idx = bundleName.lastIndexOf('/');
-        if (idx >= 0) {
-            bundleName = bundleName.substring(idx + 1);
+    void finalizePackageAssembly(@NotNull Map<PackageType, VaultPackageAssembler> packageAssemblers, @NotNull ContentPackage2FeatureModelConverter converter, @Nullable String runMode) throws Exception {
+        for (java.util.Map.Entry<PackageType, VaultPackageAssembler> entry : packageAssemblers.entrySet()) {
+            File packageFile = entry.getValue().createPackage(false);
+            converter.processContentPackageArchive(packageFile, runMode);
+            Files.delete(packageFile.toPath());
         }
-        // Remove the extension
-        int edx = bundleName.lastIndexOf('.');
-        if (edx > 0) {
-            bundleName = bundleName.substring(0, edx);
+    }
+
+    Map.Entry<ContentParser, ParserOptions> getContentParserForEntry(JarEntry entry, PathEntry pathEntry) {
+        if (entry.getName().endsWith(".json") && !pathEntry.isIgnoredImportProvider("json")) {
+            return new AbstractMap.SimpleEntry<>(new JSONContentParser(), new JSONParserOptions().withFeatures(JSONParserOptions.DEFAULT_JSON_PARSER_FEATURES));
+        } else {
+            return null;
         }
+    }
+
+    protected @NotNull ArtifactId extractArtifactId(@NotNull String bundleName, @NotNull JarFile jarFile) throws IOException {
+        String artifactId = null;
+        String version = null;
+        String groupId = null;
+        String classifier = null;
 
-        JarEntry jarEntry;
-        while ((jarEntry = jarInput.getNextJarEntry()) != null) {
+        for (Enumeration<JarEntry> e = jarFile.entries(); e.hasMoreElements();) {
+            JarEntry jarEntry = e.nextElement();
             String nextEntryName = jarEntry.getName();
 
-            String artifactId = null;
-            String version = null;
-            String groupId = null;
-            Properties properties = new Properties();
-            boolean pomXml = false;
-            if (pomPropertiesPattern.matcher(nextEntryName).matches()) {
+            if (POM_PROPERTIES_PATTERN.matcher(nextEntryName).matches()) {
                 logger.info("Reading '{}' bundle GAV from {}...", bundleName, nextEntryName);
-                properties = pomProperties;
-                properties.load(jarInput);
-
+                Properties properties = new Properties();
+                try (InputStream input = jarFile.getInputStream(jarEntry)) {
+                    properties.load(input);
+                }
                 groupId = properties.getProperty(NAME_GROUP_ID);
                 artifactId = properties.getProperty(NAME_ARTIFACT_ID);
-                version = properties.getProperty(NAME_VERSION);
+                version = properties.getProperty(PackageProperties.NAME_VERSION);
 
-            } else if (pomXmlPropertiesPattern.matcher(nextEntryName).matches()) {
-                pomXml = true;
+            } else if (POM_XML_PATTERN.matcher(nextEntryName).matches()) {
                 logger.info("Reading '{}' bundle GAV from {}...", bundleName, nextEntryName);
-                properties = pomXmlProperties;
                 String path = nextEntryName.substring(0, nextEntryName.length() - "/pom.xml".length());
                 groupId = path.substring("META-INF/maven/".length(), path.lastIndexOf('/'));
                 artifactId = path.substring(path.lastIndexOf('/') + 1);
                 if (artifactId.indexOf('-') != -1) {
                     version = artifactId.substring(artifactId.indexOf('-'));
                     artifactId = artifactId.substring(0, artifactId.indexOf('-'));
-                    properties.put(NAME_VERSION, version);
                 } else if (bundleName.indexOf('-') != -1){
                     try {
                         String versionString = bundleName.substring(bundleName.indexOf('-') + 1);
                         if (!parseVersion(versionString).equals(Version.emptyVersion)) {
                             version = versionString;
-                            properties.put(NAME_VERSION, version);
                         }
                     } catch (IllegalArgumentException ex) {
                         // Not a version
                     }
                 }
-                properties.put(NAME_GROUP_ID, groupId);
-                properties.put(NAME_ARTIFACT_ID, artifactId);
             }
 
-
             if (groupId != null && artifactId != null && version != null) {
                 // bundleName is now the bare name without extension
                 String synthesized = artifactId + "-" + version;
@@ -225,28 +481,34 @@ public final class BundleEntryHandler extends AbstractRegexEntryHandler {
                     if (synthesized.length() < bundleName.length()) {
                         String suffix = bundleName.substring(synthesized.length());
                         if (suffix.length() > 1 && suffix.startsWith("-")) {
-                            String classifier = suffix.substring(1);
-                            logger.info("Inferred classifier of '"
-                                    + artifactId
-                                    + ":"
-                                    + version
-                                    + "' to be '"
-                                    + classifier
-                                    + "'");
-                            properties.setProperty(NAME_CLASSIFIER, classifier);
+                            classifier = suffix.substring(1);
+                            logger.info("Inferred classifier of '{}:{}:{}' to be '{}'", groupId, artifactId, version, classifier);
                         }
                     }
-                    if (!pomXml) {
-                        pomProperties = properties;
-                        break;
-                    } else {
-                        pomXmlProperties = properties;
-                    }
+                    // no need to iterate further
+                    break;
                 }
             }
         }
 
-        return pomProperties.isEmpty() ? pomXmlProperties : pomProperties;
+        
+        if (groupId == null) {
+            // maybe the included jar is just an OSGi bundle but not a valid Maven artifact
+            groupId = getCheckedProperty(jarFile.getManifest(), Constants.BUNDLE_SYMBOLICNAME);
+            // Make sure there are not spaces in the name to adhere to the Maven Group Id specification
+            groupId = groupId.replace(' ', '_').replace(':', '_').replace('/', '_').replace('\\', '_');
+            if (groupId.indexOf('.') != -1) {
+                artifactId = groupId.substring(groupId.lastIndexOf('.') + 1);
+                groupId = groupId.substring(0, groupId.lastIndexOf('.'));
+            }
+            if (artifactId == null || artifactId.isEmpty()) {
+                artifactId = groupId;
+            }
+            Version osgiVersion = Version.parseVersion(getCheckedProperty(jarFile.getManifest(), Constants.BUNDLE_VERSION));
+            version = osgiVersion.getMajor() + "." + osgiVersion.getMinor() + "." + osgiVersion.getMicro() + (osgiVersion.getQualifier().isEmpty() ? "" : "-" + osgiVersion.getQualifier());
+        }
+
+        return new ArtifactId(groupId, artifactId, version, classifier, JAR_TYPE);
     }
 
     private static @NotNull String getCheckedProperty(@NotNull Manifest manifest, @NotNull String name) {
@@ -259,14 +521,4 @@ public final class BundleEntryHandler extends AbstractRegexEntryHandler {
                                          + "' property.");
     }
 
-    private static String getCheckedProperty(@NotNull Properties properties, @NotNull String name) {
-        String property = properties.getProperty(name).trim();
-        if (property != null) {
-            property = property.trim();
-        }
-        return requireNonNull(property, "Jar file can not be defined as a valid Maven artifact without specifying a valid '"
-                                         + name
-                                         + "' property.");
-    }
-
 }
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/handlers/DefaultEntryHandlersManager.java b/src/main/java/org/apache/sling/feature/cpconverter/handlers/DefaultEntryHandlersManager.java
index 36b7ce8..f6f615a 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/handlers/DefaultEntryHandlersManager.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/handlers/DefaultEntryHandlersManager.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sling.feature.cpconverter.handlers;
 
+import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter.SlingInitialContentPolicy;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -30,19 +31,20 @@ public class DefaultEntryHandlersManager implements EntryHandlersManager {
     private final List<EntryHandler> entryHandlers = new LinkedList<>();
 
     public DefaultEntryHandlersManager() {
-        this(Collections.emptyMap(), false);
+        this(Collections.emptyMap(), false, SlingInitialContentPolicy.KEEP);
     }
 
-    public DefaultEntryHandlersManager(@NotNull Map<String, String> configs, boolean enforceConfigurationsAndBundlesBelowProperFolder) {
+    public DefaultEntryHandlersManager(@NotNull Map<String, String> configs, boolean enforceConfigurationsAndBundlesBelowProperFolder, SlingInitialContentPolicy slingInitialContentPolicy) {
         ServiceLoader<EntryHandler> entryHandlersLoader = ServiceLoader.load(EntryHandler.class);
         for (EntryHandler entryHandler : entryHandlersLoader) {
             if (configs.containsKey(entryHandler.getClass().getName())) {
                 entryHandler = entryHandler.withConfig(configs.get(entryHandler.getClass().getName()));
-                if (entryHandler instanceof AbstractConfigurationEntryHandler) {
-                    ((AbstractConfigurationEntryHandler) entryHandler).setEnforceConfgurationBelowConfigFolder(enforceConfigurationsAndBundlesBelowProperFolder);
-                } else if (entryHandler instanceof BundleEntryHandler) {
-                    ((BundleEntryHandler) entryHandler).setEnforceBundlesBelowInstallFolder(enforceConfigurationsAndBundlesBelowProperFolder);
-                }
+            }
+            if (entryHandler instanceof AbstractConfigurationEntryHandler) {
+                ((AbstractConfigurationEntryHandler) entryHandler).setEnforceConfgurationBelowConfigFolder(enforceConfigurationsAndBundlesBelowProperFolder);
+            } else if (entryHandler instanceof BundleEntryHandler) {
+                ((BundleEntryHandler) entryHandler).setEnforceBundlesBelowInstallFolder(enforceConfigurationsAndBundlesBelowProperFolder);
+                ((BundleEntryHandler) entryHandler).setSlingInitialContentPolicy(slingInitialContentPolicy);
             }
             addEntryHandler(entryHandler);
         }
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DefaultPackagesEventsEmitter.java b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DefaultPackagesEventsEmitter.java
index 5a419e2..3b2433c 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DefaultPackagesEventsEmitter.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DefaultPackagesEventsEmitter.java
@@ -24,8 +24,10 @@ import java.io.FileWriter;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.Writer;
+import java.net.URI;
 import java.util.Calendar;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -285,6 +287,26 @@ public final class DefaultPackagesEventsEmitter implements PackagesEventsEmitter
             public void close() {
                 //no invocation for dependency calculation
             }
+
+            @Override
+            public boolean requiresRestart() {
+                return false;
+            }
+
+            @Override
+            public Map<String, String> getExternalHooks() {
+                return null;
+            }
+
+            @Override
+            public @NotNull Map<PackageId, URI> getDependenciesLocations() {
+                return Collections.emptyMap();
+            }
+
+            @Override
+            public long getBuildCount() {
+                return 0;
+            }
         };
     }
 
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DocViewSerializerContentHandler.java b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DocViewSerializerContentHandler.java
new file mode 100644
index 0000000..89f4a97
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DocViewSerializerContentHandler.java
@@ -0,0 +1,134 @@
+/*
+ * 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.cpconverter.vltpkg;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.AbstractMap;
+import java.util.Map;
+
+import javax.jcr.NamespaceException;
+import javax.jcr.RepositoryException;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+
+import org.apache.jackrabbit.spi.Name;
+import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException;
+import org.apache.jackrabbit.spi.commons.conversion.NameParser;
+import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
+import org.apache.jackrabbit.util.Text;
+import org.apache.jackrabbit.vault.fs.impl.io.DocViewSAXFormatter;
+import org.apache.jackrabbit.vault.fs.io.DocViewFormat;
+import org.apache.jackrabbit.vault.util.xml.serialize.FormattingXmlStreamWriter;
+import org.apache.sling.contentparser.api.ContentHandler;
+
+/**
+ * Similar to {@link DocViewSAXFormatter} but works outside a repository-based context on the input generated through {@link ContentHandler} callbacks.
+ * Throws {@link DocViewSerializerContentHandlerException} in case serialization fails for some reason.
+ */
+public class DocViewSerializerContentHandler implements ContentHandler, AutoCloseable {
+
+    private final XMLStreamWriter writer;
+    private final JcrNamespaceRegistry nsRegistry;
+    private String currentPath = "/";
+    boolean isFirstElement = true;
+
+    public DocViewSerializerContentHandler(OutputStream outputStream, JcrNamespaceRegistry nsRegistry) {
+        this.nsRegistry = nsRegistry;
+        try {
+            writer = FormattingXmlStreamWriter.create(outputStream, new DocViewFormat().getXmlOutputFormat());
+            writer.writeStartDocument();
+            
+        } catch (XMLStreamException e) {
+            throw new DocViewSerializerContentHandlerException("Can not start document", e);
+        }
+    }
+
+    @Override
+    public void resource(String path, Map<String, Object> properties) {
+        // split path in parent and name
+        String parent = Text.getRelativeParent(path, 1);
+        String name = Text.getName(path);
+        if (name.isEmpty()) {
+            name = "jcr:root";
+        }
+
+        if (!parent.equals(currentPath)) {
+            closeParents(parent);
+        }
+        currentPath = path;
+        try {
+            // now split by prefix and local name
+            Map.Entry<String, Name> prefixAndQualifiedName = resolvePrefixedName(name);
+            writer.writeStartElement(prefixAndQualifiedName.getKey(), prefixAndQualifiedName.getValue().getNamespaceURI(), prefixAndQualifiedName.getValue().getLocalName());
+            if (isFirstElement) {
+                for (String prefix : nsRegistry.getPrefixes()) {
+                    writer.writeNamespace(prefix, nsRegistry.getURI(prefix));
+                }
+                isFirstElement = false;
+            }
+            for (Map.Entry<String, Object> property : properties.entrySet()) {
+                // now split by prefix and local name
+                prefixAndQualifiedName = resolvePrefixedName(property.getKey());
+                writer.writeAttribute(prefixAndQualifiedName.getKey(), prefixAndQualifiedName.getValue().getNamespaceURI(), prefixAndQualifiedName.getValue().getLocalName(), ValueConverter.toString(property.getKey(), property.getValue()));
+            }
+        } catch (XMLStreamException e) {
+            throw new DocViewSerializerContentHandlerException("Can not start element", e);
+        } catch (RepositoryException e) {
+            throw new DocViewSerializerContentHandlerException("Can not emit namespace declarations", e);
+        }
+    }
+
+    public void closeParents(String stopAtParent) {
+        try {
+            while (!currentPath.equals(stopAtParent)) {
+                writer.writeEndElement();
+                String newCurrentPath = Text.getRelativeParent(currentPath, 1);
+                if (newCurrentPath.equals(currentPath)) {
+                    break;
+                } else {
+                    currentPath = newCurrentPath;
+                }
+            }
+        } catch (XMLStreamException e) {
+            throw new DocViewSerializerContentHandlerException("Can not end element", e);
+        }
+    }
+
+    Map.Entry<String, Name> resolvePrefixedName(String name) {
+        int posColon = name.indexOf(':');
+        if (posColon == -1) {
+            return new AbstractMap.SimpleEntry<>("", NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, name));
+        }
+        try {
+            String prefix = name.substring(0, posColon);
+            return new AbstractMap.SimpleEntry<>(prefix, NameParser.parse(name, nsRegistry, NameFactoryImpl.getInstance()));
+        } catch (IllegalNameException|NamespaceException e) {
+            throw new DocViewSerializerContentHandlerException("Could not resolve namespace URI for name " + name, e);
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        try {
+            closeParents("");
+            writer.writeEndDocument();
+        } catch (XMLStreamException e) {
+            throw new DocViewSerializerContentHandlerException("Can not end document", e);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DocViewSerializerContentHandlerException.java b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DocViewSerializerContentHandlerException.java
new file mode 100644
index 0000000..6f9d911
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DocViewSerializerContentHandlerException.java
@@ -0,0 +1,39 @@
+/*
+ * 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.cpconverter.vltpkg;
+
+public class DocViewSerializerContentHandlerException extends RuntimeException {
+
+    /**
+     * 
+     */
+    private static final long serialVersionUID = 1L;
+
+    public DocViewSerializerContentHandlerException(String message, Throwable cause, boolean enableSuppression,
+            boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+
+    public DocViewSerializerContentHandlerException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public DocViewSerializerContentHandlerException(String message) {
+        super(message);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/JcrNamespaceRegistry.java b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/JcrNamespaceRegistry.java
new file mode 100644
index 0000000..035ffbc
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/JcrNamespaceRegistry.java
@@ -0,0 +1,103 @@
+/*
+ * 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.cpconverter.vltpkg;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.NamespaceException;
+import javax.jcr.NamespaceRegistry;
+import javax.jcr.RepositoryException;
+import javax.jcr.ValueFactory;
+import javax.jcr.nodetype.NodeTypeManager;
+
+import org.apache.jackrabbit.commons.cnd.CndImporter;
+import org.apache.jackrabbit.commons.cnd.ParseException;
+import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
+import org.jetbrains.annotations.NotNull;
+
+/** Simple namespace registry backed by a map */
+public class JcrNamespaceRegistry implements NamespaceRegistry, NamespaceResolver {
+
+    private final Map<String, String> prefixUriMapping;
+    private final Collection<String> registeredCndSystemIds;
+    
+    public JcrNamespaceRegistry() {
+        prefixUriMapping = new HashMap<>();
+        prefixUriMapping.put(PREFIX_JCR, NAMESPACE_JCR);
+        prefixUriMapping.put(PREFIX_MIX, NAMESPACE_MIX);
+        prefixUriMapping.put(PREFIX_NT, NAMESPACE_NT);
+        prefixUriMapping.put(PREFIX_XML, NAMESPACE_XML);
+        // referencing from org.apache.sling.api.SlingConstants would require an additional dependency
+        prefixUriMapping.put("sling", "http://sling.apache.org/");
+        registeredCndSystemIds = new ArrayList<>();
+    }
+
+    public void registerCnd(Reader reader, String systemId) throws ParseException, RepositoryException, IOException {
+        NodeTypeManager ntManager = null;
+        ValueFactory valueFactory = null;
+        CndImporter.registerNodeTypes(reader, systemId, ntManager, this, valueFactory, false);
+        registeredCndSystemIds.add(systemId);
+    }
+
+    @Override
+    public void registerNamespace(String prefix, String uri)
+            throws RepositoryException {
+        String oldUri = prefixUriMapping.putIfAbsent(prefix, uri);
+        if (oldUri != null && !oldUri.equals(uri)) {
+            throw new RepositoryException("Prefix " + prefix + " already used for another uri!");
+        }
+    }
+
+    @Override
+    public void unregisterNamespace(String prefix)
+            throws RepositoryException {
+        throw new UnsupportedOperationException("Unregistering namespaces is unsupported");
+    }
+
+    @Override
+    public String[] getPrefixes() throws RepositoryException {
+        return prefixUriMapping.keySet().toArray(new String[0]);
+    }
+
+    @Override
+    public String[] getURIs() throws RepositoryException {
+        return prefixUriMapping.values().toArray(new String[0]);
+    }
+
+    @Override
+    public String getURI(String prefix) throws NamespaceException {
+        String uri = prefixUriMapping.get(prefix);
+        if (uri == null) {
+            throw new NamespaceException("No registered URI found for prefix " + prefix);
+        }
+        return uri;
+    }
+
+    @Override
+    public String getPrefix(String uri) throws NamespaceException {
+        throw new UnsupportedOperationException("This lookup direction is unsupported");
+    }
+
+    public @NotNull Collection<String> getRegisteredCndSystemIds() {
+        return registeredCndSystemIds;
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/SingleFileArchive.java b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/SingleFileArchive.java
new file mode 100644
index 0000000..1073957
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/SingleFileArchive.java
@@ -0,0 +1,177 @@
+/*
+ * 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.cpconverter.vltpkg;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.apache.commons.io.function.IOSupplier;
+import org.apache.jackrabbit.vault.fs.api.VaultInputSource;
+import org.apache.jackrabbit.vault.fs.config.DefaultMetaInf;
+import org.apache.jackrabbit.vault.fs.config.MetaInf;
+import org.apache.jackrabbit.vault.fs.io.Archive;
+import org.apache.jackrabbit.vault.util.FileInputSource;
+
+/**
+ * Best-effort implementation of a FileVault archive only containing a single file.
+ * Lots of information is obviously not available here (i.e. metadata, ....)
+ * Used for passing Sling Initial Content in bundles to the EntryHandlers
+ */
+public class SingleFileArchive implements Archive {
+
+    private final Path file;
+    private final InputStream inputStream;
+    private final IOSupplier<Path> tmpFileSupplier;
+    private Path tmpFile;
+    private final String relativePath;
+
+    public static SingleFileArchive fromPathOrInputStream(Path path, InputStream inputStream, IOSupplier<Path> tmpFileSupplier, String relativePath) {
+        if (path != null) {
+            return new SingleFileArchive(path, relativePath);
+        } else if (inputStream != null) {
+            return new SingleFileArchive(inputStream, tmpFileSupplier, relativePath);
+        } else {
+            throw new IllegalArgumentException("Either file or inputStream must be non-null!");
+        }
+    }
+
+    private SingleFileArchive(InputStream inputStream, IOSupplier<Path> tmpFileSupplier, String relativePath) {
+        this.file = null;
+        this.inputStream = inputStream;
+        this.tmpFileSupplier = tmpFileSupplier;
+        this.relativePath = relativePath;
+    }
+
+    private SingleFileArchive(Path file, String relativePath) {
+        this.file = file;
+        this.inputStream = null;
+        this.tmpFileSupplier = null;
+        this.relativePath = relativePath;
+    }
+
+    @Override
+    public void open(boolean strict) throws IOException {
+        // noop
+    }
+
+    @Override
+    public InputStream openInputStream(Entry entry) throws IOException {
+        if (!(entry instanceof SingleFileEntry)) {
+            throw new IllegalArgumentException("Can only open input stream for SingleFileEntry, but given entry is " + entry.getClass());
+        }
+        return Files.newInputStream(getTmpFile());
+    }
+
+    private Path getTmpFile() throws IOException {
+        if (file != null) {
+            return file;
+        } else if (tmpFile != null) {
+            return tmpFile;
+        } else if (inputStream != null) {
+            tmpFile = tmpFileSupplier.get();
+            Files.copy(inputStream, tmpFile, StandardCopyOption.REPLACE_EXISTING);
+            return tmpFile;
+        } else {
+            throw new IllegalArgumentException("Either file or inputStream must be non-null!");
+        }
+    }
+
+    @Override
+    public VaultInputSource getInputSource(Entry entry) throws IOException {
+        if (!(entry instanceof SingleFileEntry)) {
+            throw new IllegalArgumentException("Can only open input stream for SingleFileEntry, but given entry is " + entry.getClass());
+        }
+        
+        return new FileInputSource(getTmpFile().toFile());
+    }
+
+    @Override
+    public Entry getJcrRoot() throws IOException {
+        return null;
+    }
+
+    @Override
+    public Entry getRoot() throws IOException {
+        return new SingleFileEntry(this);
+    }
+
+    @Override
+    public MetaInf getMetaInf() {
+        return new DefaultMetaInf();
+    }
+
+    @Override
+    public Entry getEntry(String path) throws IOException {
+        if (path.equals(relativePath)) {
+            return new SingleFileEntry(this);
+        }
+        return null;
+    }
+
+    @Override
+    public Archive getSubArchive(String root, boolean asJcrRoot) throws IOException {
+        return null;
+    }
+
+    @Override
+    public void close() {
+        // delete file on close
+        if (tmpFile != null) {
+            try {
+                Files.delete(tmpFile);
+            } catch (IOException e) {
+                throw new IllegalStateException("Can not delete temporary file");
+            }
+        }
+    }
+
+    public static class SingleFileEntry implements Entry {
+
+        SingleFileArchive archive;
+        
+        SingleFileEntry(SingleFileArchive archive) {
+            this.archive = archive;
+        }
+ 
+        @Override
+        public String getName() {
+            // use forward slashes as separators
+            return archive.relativePath;
+        }
+
+        @Override
+        public boolean isDirectory() {
+            return false;
+        }
+
+        @Override
+        public Collection<? extends Entry> getChildren() {
+            return Collections.emptyList();
+        }
+
+        @Override
+        public Entry getChild(String name) {
+            return null;
+        }
+        
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/ValueConverter.java b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/ValueConverter.java
new file mode 100644
index 0000000..7b59ab7
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/ValueConverter.java
@@ -0,0 +1,494 @@
+/*
+ * 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.cpconverter.vltpkg;
+
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.math.BigDecimal;
+import java.net.URI;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.UUID;
+
+import javax.jcr.Binary;
+import javax.jcr.Item;
+import javax.jcr.ItemVisitor;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.PropertyDefinition;
+
+import org.apache.jackrabbit.util.ISO8601;
+import org.apache.jackrabbit.vault.util.DocViewProperty;
+
+/**
+ * Converts an value to string for a content property in XML including type prefix.
+ * Is a temporary helper which can be removed once https://issues.apache.org/jira/browse/JCRVLT-516 is solved.
+ */
+final class ValueConverter {
+
+  static final String PN_PRIVILEGES = "rep:privileges";
+
+  private ValueConverter() {
+      // only static methods
+  }
+  /**
+   * Converts an object to a string representation.
+   * Supported are String, Boolean, Integer, Long, Double, BigDecimal, Date, Calendar and arrays of them.
+   * @param value value
+   * @return Converted value
+   */
+  public static String toString(String propertyName, Object value) {
+    if (value == null) {
+      return "";
+    }
+
+    Value[] values;
+    boolean multiple = value.getClass().isArray();
+    if (multiple) {
+      values = new Value[Array.getLength(value)];
+      int lastPropertyType = PropertyType.UNDEFINED;
+      for (int i = 0; i < values.length; i++) {
+        values[i] = toValue(propertyName, Array.get(value, i));
+        if (lastPropertyType == PropertyType.UNDEFINED) {
+          lastPropertyType = values[i].getType();
+        }
+        else if (lastPropertyType != values[i].getType()) {
+          throw new RuntimeException("Mixing different value types within array not allowed: " +
+              PropertyType.nameFromValue(lastPropertyType) + ", " + PropertyType.nameFromValue(values[i].getType())
+              + ", propertyName=" + propertyName + ", value=" + value);
+        }
+      }
+    }
+    else {
+      values = new Value[] { toValue(propertyName, value) };
+    }
+
+    Property prop = new MockProperty(propertyName, multiple, values);
+    try {
+      return DocViewProperty.format(prop);
+    }
+    catch (RepositoryException ex) {
+      throw new RuntimeException("Unable to format property value (" + propertyName + "): " + value, ex);
+    }
+  }
+
+  private static Value toValue(String propertyName, Object value) {
+    if (value instanceof String) {
+      if (PN_PRIVILEGES.equals(propertyName)) {
+        return new MockValue(value.toString(), PropertyType.NAME);
+      }
+      else {
+        return new MockValue(value.toString(), PropertyType.STRING);
+      }
+    }
+    if (value instanceof Boolean) {
+      return new MockValue(((Boolean)value).toString(), PropertyType.BOOLEAN);
+    }
+    if (value instanceof Integer || value instanceof Long) {
+      return new MockValue(Long.toString(((Number)value).longValue()), PropertyType.LONG);
+    }
+    if (value instanceof Float || value instanceof Double) {
+      return new MockValue(Double.toString(((Number)value).doubleValue()), PropertyType.DECIMAL);
+    }
+    if (value instanceof BigDecimal) {
+      return new MockValue(((BigDecimal)value).toString(), PropertyType.DECIMAL);
+    }
+    if (value instanceof Date) {
+      Calendar calendar = Calendar.getInstance();
+      calendar.setTime((Date)value);
+      return new MockValue(ISO8601.format(calendar), PropertyType.DATE);
+    }
+    if (value instanceof Calendar) {
+      return new MockValue(ISO8601.format((Calendar)value), PropertyType.DATE);
+    }
+    if (value instanceof UUID) {
+      return new MockValue(((UUID)value).toString(), PropertyType.REFERENCE);
+    }
+    if (value instanceof URI) {
+      return new MockValue(((URI)value).toString(), PropertyType.URI);
+    }
+    throw new IllegalArgumentException("Type not supported: " + value.getClass().getName());
+  }
+
+
+  /**
+   * Mock implementations of JCR property and value to be handed over to {@link DocViewProperty#format(Property)}
+   * method.
+   */
+  private static class MockProperty implements Property, PropertyDefinition {
+
+    private final String name;
+    private final boolean multiple;
+    private final Value[] values;
+
+    MockProperty(String name, boolean multiple, Value[] values) {
+      this.name = name;
+      this.multiple = multiple;
+      this.values = values;
+    }
+
+    @Override
+    public String getName() {
+      return name;
+    }
+
+    @Override
+    public int getType() {
+      if (values.length > 0) {
+        return values[0].getType();
+      }
+      return PropertyType.UNDEFINED;
+    }
+
+    @Override
+    public boolean isMultiple() {
+      return multiple;
+    }
+
+    @Override
+    public Value getValue() throws ValueFormatException {
+      if (multiple) {
+        throw new ValueFormatException("Property is multiple.");
+      }
+      return values[0];
+    }
+
+    @Override
+    public Value[] getValues() throws ValueFormatException {
+      if (!multiple) {
+        throw new ValueFormatException("Property is not multiple.");
+      }
+      return values;
+    }
+
+    @Override
+    public PropertyDefinition getDefinition() {
+      return this;
+    }
+
+
+    // -- unsupported methods --
+
+    @Override
+    public String getPath() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Item getAncestor(int depth) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Node getParent() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getDepth() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Session getSession() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isNode() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isNew() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isModified() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isSame(Item otherItem) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void accept(ItemVisitor visitor) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void save() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void refresh(boolean keepChanges) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void remove() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(Value value) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(Value[] value) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(String value) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(String[] value) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(InputStream value) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(Binary value) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(long value) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(double value) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(BigDecimal value) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(Calendar value) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(boolean value) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void setValue(Node value) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getString() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public InputStream getStream() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Binary getBinary() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long getLong() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public double getDouble() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public BigDecimal getDecimal() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Calendar getDate() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean getBoolean() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Node getNode() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Property getProperty() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long getLength() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long[] getLengths() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public NodeType getDeclaringNodeType() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isAutoCreated() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isMandatory() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getOnParentVersion() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isProtected() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getRequiredType() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] getValueConstraints() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Value[] getDefaultValues() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String[] getAvailableQueryOperators() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isFullTextSearchable() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isQueryOrderable() {
+      throw new UnsupportedOperationException();
+    }
+
+  }
+
+  private static class MockValue implements Value {
+
+    private final String value;
+    private final int type;
+
+    MockValue(String value, int type) {
+      this.value = value;
+      this.type = type;
+    }
+
+    @Override
+    public String getString() throws ValueFormatException, IllegalStateException, RepositoryException {
+      return value;
+    }
+
+    @Override
+    public int getType() {
+      return type;
+    }
+
+
+    // -- unsupported methods --
+
+    @Override
+    public InputStream getStream() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Binary getBinary() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long getLong() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public double getDouble() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public BigDecimal getDecimal() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Calendar getDate() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean getBoolean() {
+      throw new UnsupportedOperationException();
+    }
+
+  }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/VaultPackageAssembler.java b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/VaultPackageAssembler.java
index 97c2fa5..065f10b 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/VaultPackageAssembler.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/VaultPackageAssembler.java
@@ -32,12 +32,13 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Properties;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Consumer;
+import java.util.function.BiConsumer;
 import java.util.function.Predicate;
 import java.util.jar.JarEntry;
 import java.util.jar.JarFile;
@@ -61,6 +62,7 @@ import org.apache.jackrabbit.vault.packaging.PackageProperties;
 import org.apache.jackrabbit.vault.packaging.PackageType;
 import org.apache.jackrabbit.vault.packaging.VaultPackage;
 import org.apache.jackrabbit.vault.util.Constants;
+import org.apache.jackrabbit.vault.util.PlatformNameFormat;
 import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
 import org.apache.sling.feature.cpconverter.handlers.EntryHandler;
 import org.jetbrains.annotations.NotNull;
@@ -71,6 +73,8 @@ import org.slf4j.LoggerFactory;
 public class VaultPackageAssembler implements EntryHandler {
 
     private static final Pattern OSGI_BUNDLE_PATTERN = Pattern.compile("(jcr_root)?/apps/[^/]+/install(\\.([^/]+))?/.+\\.jar");
+    
+    public static final String VERSION_SUFFIX = '-' + PACKAGE_CLASSIFIER;
 
     private static final Logger log = LoggerFactory.getLogger(VaultPackageAssembler.class);
 
@@ -82,27 +86,31 @@ public class VaultPackageAssembler implements EntryHandler {
         }
     }
 
+    /**
+     * Creates a new package assembler based on an existing package.
+     * Takes over properties and filter rules from existing package.
+     * @param tempDir the temp dir
+     * @param vaultPackage the package to take as blueprint
+     * @param removeInstallHooks whether to remove install hooks or not
+     * @return the package assembler
+     */
     public static @NotNull VaultPackageAssembler create(@NotNull File tempDir, @NotNull VaultPackage vaultPackage, boolean removeInstallHooks) {
         return create(tempDir, vaultPackage, Objects.requireNonNull(vaultPackage.getMetaInf().getFilter()), removeInstallHooks);
     }
-    
+
+    /**
+     * Creates a new package assembler based on an existing package.
+     * Takes over properties from existing package.
+     * @param baseTempDir the temp dir
+     * @param vaultPackage the package to take as blueprint
+     * @param filter the filter with which to initialize the new package
+     * @param removeInstallHooks whether to remove install hooks or not
+     * @return the package assembler
+     */
     private static @NotNull VaultPackageAssembler create(@NotNull File baseTempDir, @NotNull VaultPackage vaultPackage, @NotNull WorkspaceFilter filter, boolean removeInstallHooks) {
         final File tempDir = new File(baseTempDir, "synthetic-content-packages_" + System.currentTimeMillis());
         PackageId packageId = vaultPackage.getId();
-        String fileName = packageId.toString().replaceAll("/", "-").replaceAll(":", "-") + "-" + vaultPackage.getFile().getName();
-        File storingDirectory = new File(tempDir, fileName + "-deflated");
-        if(storingDirectory.exists()) {
-            try {
-                FileUtils.deleteDirectory(storingDirectory);
-            } catch(IOException e) {
-                throw new FolderDeletionException("Unable to delete existing deflated folder: '" + storingDirectory + "'", e);
-            }
-        }
-        // avoid any possible Stream is not a content package. Missing 'jcr_root' error
-        File jcrRootDirectory = new File(storingDirectory, ROOT_DIR);
-        if (!jcrRootDirectory.mkdirs() && jcrRootDirectory.isDirectory()) {
-            throw new IllegalStateException("Unable to create jcr root dir: " + jcrRootDirectory);
-        }
+        File storingDirectory = initStoringDirectory(packageId, tempDir);
 
         Properties properties = new Properties();
         Map<Object, Object> originalPackageProperties = vaultPackage.getMetaInf().getProperties();
@@ -117,8 +125,7 @@ public class VaultPackageAssembler implements EntryHandler {
         properties.putAll(originalPackageProperties);
         properties.setProperty(PackageProperties.NAME_VERSION,
                                vaultPackage.getId().getVersion().toString()
-                                                             + '-'
-                                                             + PACKAGE_CLASSIFIER);
+                                                             + VERSION_SUFFIX);
 
         Set<Dependency> dependencies = getDependencies(vaultPackage);
 
@@ -127,6 +134,44 @@ public class VaultPackageAssembler implements EntryHandler {
         return assembler;
     }
 
+    /**
+     * Creates a new package assembler.
+     * @param baseTempDir the temp dir
+     * @param packageId the package id from which to generate a minimal properties.xml
+     * @param description the description which should end up in the package properties
+     * @return the package assembler
+     */
+    public static @NotNull VaultPackageAssembler create(@NotNull File baseTempDir, @NotNull PackageId packageId, String description) {
+        final File tempDir = new File(baseTempDir, "synthetic-content-packages_" + System.currentTimeMillis());
+        File storingDirectory = initStoringDirectory(packageId, tempDir);
+        Properties props = new Properties();
+        // generate minimal properties (http://jackrabbit.apache.org/filevault/properties.html)
+        props.put(PackageProperties.NAME_GROUP, packageId.getGroup());
+        props.put(PackageProperties.NAME_NAME, packageId.getName());
+        props.put(PackageProperties.NAME_VERSION, packageId.getVersionString() + VERSION_SUFFIX);
+
+        props.put(PackageProperties.NAME_DESCRIPTION, description);
+        return new VaultPackageAssembler(tempDir, storingDirectory, props, new HashSet<>(), false);
+    }
+
+    static @NotNull File initStoringDirectory(PackageId packageId, @NotNull File tempDir) {
+        String fileName = packageId.toString().replace('/', '-').replace(':', '-');
+        File storingDirectory = new File(tempDir, fileName + "-deflated");
+        if(storingDirectory.exists()) {
+            try {
+                FileUtils.deleteDirectory(storingDirectory);
+            } catch(IOException e) {
+                throw new IllegalStateException("Unable to delete existing deflated folder: '" + storingDirectory + "'", e);
+            }
+        }
+        // avoid any possible Stream is not a content package. Missing 'jcr_root' error
+        File jcrRootDirectory = new File(storingDirectory, ROOT_DIR);
+        if (!jcrRootDirectory.mkdirs() && jcrRootDirectory.isDirectory()) {
+            throw new IllegalStateException("Unable to create jcr root dir: " + jcrRootDirectory);
+        }
+        return storingDirectory;
+    }
+
     private final DefaultWorkspaceFilter filter = new DefaultWorkspaceFilter();
 
     private final Set<Dependency> dependencies;
@@ -181,6 +226,10 @@ public class VaultPackageAssembler implements EntryHandler {
         }
     }
 
+    public DefaultWorkspaceFilter getFilter() {
+        return filter;
+    }
+
     public void addEntry(@NotNull String path, @NotNull Archive archive, @NotNull Entry entry) throws IOException {
         try (InputStream input = Objects.requireNonNull(archive.openInputStream(entry))) {
             addEntry(path, input);
@@ -236,6 +285,10 @@ public class VaultPackageAssembler implements EntryHandler {
     }
 
     public @NotNull File createPackage() throws IOException {
+        return createPackage(false);
+    }
+
+    public @NotNull File createPackage(boolean generateFilters) throws IOException {
         // generate the Vault properties XML file
 
         File metaDir = new File(storingDirectory, META_DIR);
@@ -263,8 +316,11 @@ public class VaultPackageAssembler implements EntryHandler {
             properties.storeToXML(fos, null);
         }
 
-        // generate the Vault filter XML file based on new contents of the package
-        computeFilters(storingDirectory);
+        if (generateFilters) {
+            // generate the Vault filter XML file based on new contents of the package
+            computeFilters(storingDirectory);
+        }
+
         File xmlFilter = new File(metaDir, FILTER_XML);
         try (InputStream input = filter.getSource();
                 FileOutputStream output = new FileOutputStream(xmlFilter)) {
@@ -274,7 +330,6 @@ public class VaultPackageAssembler implements EntryHandler {
         // create the target archiver
         final String destFileName = storingDirectory.getName().substring(0, storingDirectory.getName().lastIndexOf('-'));
         final File destFile = new File(this.tmpDir, destFileName);
-
         final File manifestFile = new File(storingDirectory, JarFile.MANIFEST_NAME.replace('/', File.separatorChar));
         Manifest manifest = null;
         if ( manifestFile.exists() ) {
@@ -323,7 +378,7 @@ public class VaultPackageAssembler implements EntryHandler {
         }
         AtomicBoolean foundMutableFiles = new AtomicBoolean();
         AtomicBoolean foundImmutableFiles  = new AtomicBoolean();
-        forEachDirectoryBelowJcrRoot(outputDirectory, child -> {
+        forEachDirectoryBelowJcrRoot(outputDirectory, (child, base) -> {
             if (child.getName().equals("apps") || child.getName().equals("libs")) {
                 foundImmutableFiles.weakCompareAndSet(false, true);
             } else {
@@ -341,22 +396,22 @@ public class VaultPackageAssembler implements EntryHandler {
     }
 
     private void computeFilters(@NotNull File outputDirectory) {
-        forEachDirectoryBelowJcrRoot(outputDirectory, child -> {
+        forEachDirectoryBelowJcrRoot(outputDirectory, (child, base) -> {
                 TreeNode node = lowestCommonAncestor(new TreeNode(child));
                 File lowestCommonAncestor = node != null ? node.val : null;
                 if (lowestCommonAncestor != null) {
-                    String root = outputDirectory.toURI().relativize(lowestCommonAncestor.toURI()).getPath();
-
+                    String root = "/" + PlatformNameFormat.getRepositoryPath(base.toURI().relativize(lowestCommonAncestor.toURI()).getPath(), true);
                     filter.add(new PathFilterSet(root));
                 }
             });
     }
-    
-    private static void forEachDirectoryBelowJcrRoot(File outputDirectory, Consumer<File> consumer) {
+
+    private static void forEachDirectoryBelowJcrRoot(File outputDirectory, BiConsumer<File, File> consumer) {
         File jcrRootDir = new File(outputDirectory, ROOT_DIR);
         if (jcrRootDir.exists() && jcrRootDir.isDirectory()) {
             for (File child : jcrRootDir.listFiles((FileFilter)DirectoryFileFilter.INSTANCE)) {
-                consumer.accept(child);
+                // calls consumer with absolute files
+                consumer.accept(child, jcrRootDir);
             }
         }
     }
@@ -410,14 +465,4 @@ public class VaultPackageAssembler implements EntryHandler {
         }
 
     }
-
-    public static class FolderDeletionException extends RuntimeException {
-        public FolderDeletionException(@NotNull String message) {
-            super(message);
-        }
-
-        public FolderDeletionException(@NotNull String message, @NotNull Throwable cause) {
-            super(message, cause);
-        }
-    }
 }
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/VaultPackageUtils.java b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/VaultPackageUtils.java
index 4662f74..d5aea4b 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/VaultPackageUtils.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/VaultPackageUtils.java
@@ -47,31 +47,33 @@ public class VaultPackageUtils {
 
         // borrowed from org.apache.jackrabbit.vault.fs.io.AbstractExporter
         WorkspaceFilter filter = vaultPackage.getMetaInf().getFilter();
-
-        boolean hasApps = false;
-        boolean hasOther = false;
         if (filter != null) {
             for (PathFilterSet p : filter.getFilterSets()) {
                 if ("cleanup".equals(p.getType())) {
                     continue;
                 }
                 String root = p.getRoot();
-                if ("/apps".equals(root)
-                        || root.startsWith("/apps/")
-                        || "/libs".equals(root)
-                        || root.startsWith("/libs/")) {
-                    hasApps = true;
+                @NotNull PackageType newPackageType = detectPackageType(root);
+                if (packageType != null && packageType != newPackageType) {
+                    // bail out once we ended up with mixed
+                    return PackageType.MIXED;
                 } else {
-                    hasOther = true;
+                    packageType = newPackageType;
                 }
             }
-            if (hasApps && !hasOther) {
-                return PackageType.APPLICATION;
-            } else if (hasOther && !hasApps) {
-                return PackageType.CONTENT;
-            }
         }
-        return PackageType.MIXED;
+        return packageType != null ? packageType : PackageType.MIXED;
+    }
+
+    public static @NotNull PackageType detectPackageType(String path) {
+        if ("/apps".equals(path)
+                || path.startsWith("/apps/")
+                || "/libs".equals(path)
+                || path.startsWith("/libs/")) {
+            return PackageType.APPLICATION;
+        } else {
+            return PackageType.CONTENT;
+        }
     }
 
     public static @NotNull Set<Dependency> getDependencies(@NotNull VaultPackage vaultPackage) {
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/handlers/AbstractBundleEntryHandlerTest.java b/src/test/java/org/apache/sling/feature/cpconverter/handlers/AbstractBundleEntryHandlerTest.java
new file mode 100644
index 0000000..a4b1162
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/cpconverter/handlers/AbstractBundleEntryHandlerTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.cpconverter.handlers;
+
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+
+import org.apache.jackrabbit.vault.fs.io.Archive;
+import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
+import org.apache.sling.feature.cpconverter.artifacts.ArtifactsDeployer;
+import org.apache.sling.feature.cpconverter.features.FeaturesManager;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+
+/**
+ * Base class for tests against BundleEntryHandler.
+ * Must be executed with MockitoJUnitRunner.
+ *
+ */
+public abstract class AbstractBundleEntryHandlerTest {
+
+    @Rule
+    public TemporaryFolder tmpFolder = new TemporaryFolder();
+
+    @Mock 
+    Archive.Entry entry;
+    @Mock 
+    Archive archive;
+    @Spy 
+    ContentPackage2FeatureModelConverter converter;
+    @Spy 
+    FeaturesManager featuresManager;
+    
+    BundleEntryHandler handler;
+
+    protected void setUpArchive(String entryPath, String resourcePath) throws IOException {
+        when(entry.getName()).thenReturn(entryPath);
+        when(archive.openInputStream(entry)).thenReturn(BundleEntryHandlerGAVTest.class.getResourceAsStream(resourcePath));
+    }
+
+    @Before
+    public void setUp() throws IOException {
+        handler = new BundleEntryHandler();
+        ArtifactsDeployer deployer = Mockito.spy(ArtifactsDeployer.class);
+        when(converter.getArtifactsDeployer()).thenReturn(deployer);
+        when(converter.getTempDirectory()).thenReturn(tmpFolder.getRoot());
+        featuresManager = Mockito.spy(FeaturesManager.class);
+        when(converter.getFeaturesManager()).thenReturn(featuresManager);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandleSlingInitialContentTest.java b/src/test/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandleSlingInitialContentTest.java
new file mode 100644
index 0000000..77aaa70
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandleSlingInitialContentTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.cpconverter.handlers;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Map;
+import java.util.jar.JarFile;
+
+import org.apache.jackrabbit.vault.fs.io.Archive;
+import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
+import org.apache.jackrabbit.vault.packaging.PackageId;
+import org.apache.jackrabbit.vault.packaging.VaultPackage;
+import org.apache.jackrabbit.vault.packaging.impl.PackageManagerImpl;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter.SlingInitialContentPolicy;
+import org.apache.sling.feature.cpconverter.artifacts.SimpleFolderArtifactsDeployer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
+import org.mockito.Captor;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+
+@RunWith(MockitoJUnitRunner.class)
+public class BundleEntryHandleSlingInitialContentTest extends AbstractBundleEntryHandlerTest {
+
+    @Captor
+    ArgumentCaptor<Dictionary<String, Object>> dictionaryCaptor;
+
+    @Test
+    public void testSlingInitialContent() throws Exception {
+        setUpArchive("/jcr_root/apps/gav/install/io.wcm.handler.media-1.11.6.jar", "io.wcm.handler.media-1.11.6.jar");
+        DefaultEntryHandlersManager handlersManager = new DefaultEntryHandlersManager();
+        converter.setEntryHandlersManager(handlersManager);
+        Map<String, String> namespaceRegistry = Collections.singletonMap("granite", "http://www.adobe.com/jcr/granite/1.0");
+        when(featuresManager.getNamespaceUriByPrefix()).thenReturn(namespaceRegistry);
+
+        File targetFolder = tmpFolder.newFolder();
+        when(converter.getArtifactsDeployer()).thenReturn(new SimpleFolderArtifactsDeployer(targetFolder));
+        handler.setSlingInitialContentPolicy(SlingInitialContentPolicy.EXTRACT_AND_REMOVE);
+        handler.handle("/jcr_root/apps/gav/install/io.wcm.handler.media-1.11.6.jar", archive, entry, converter);
+
+        // verify generated bundle
+        try (JarFile jarFile = new JarFile(new File(targetFolder, "io.wcm.handler.media-1.11.6-cp2fm-converted.jar"))) {
+            String bundleVersion = jarFile.getManifest().getMainAttributes().getValue(Constants.BUNDLE_VERSION);
+            assertNotNull(bundleVersion);
+            assertNull(jarFile.getManifest().getMainAttributes().getValue("Sling-Initial-Content"));
+            assertEquals("_cp2fm-converted", Version.parseVersion(bundleVersion).getQualifier());
+            // make sure the initial content is no longer contained
+            assertNull(jarFile.getEntry("SLING-INF/app-root/"));
+        }
+        // verify generated package
+        try (VaultPackage vaultPackage = new PackageManagerImpl().open(new File(targetFolder, "io.wcm.handler.media-apps-1.11.6-cp2fm-converted.zip"));
+             Archive archive = vaultPackage.getArchive()) {
+            archive.open(true);
+            PackageId targetId = PackageId.fromString("io.wcm:io.wcm.handler.media-apps:1.11.6-cp2fm-converted");
+            assertEquals(targetId, vaultPackage.getId());
+            Entry entry = archive.getEntry("jcr_root/apps/wcm-io/handler/media/components/global/include/responsiveImageSettings.xml");
+            assertNotNull("Archive does not contain expected item", entry);
+        }
+        // verify nothing else has been deployed
+        assertEquals(2, targetFolder.list().length);
+        // verify changed id
+        Mockito.verify(featuresManager).addArtifact(null, ArtifactId.fromMvnId("io.wcm:io.wcm.handler.media:1.11.6-cp2fm-converted"), null);
+    }
+
+    @Test
+    public void testSlingInitialContentContainingConfigurationExtractAndRemove() throws Exception {
+        setUpArchive("/jcr_root/apps/gav/install/composum-nodes-config-2.5.3.jar", "composum-nodes-config-2.5.3.jar");
+        DefaultEntryHandlersManager handlersManager = new DefaultEntryHandlersManager();
+        converter.setEntryHandlersManager(handlersManager);
+        handler.setSlingInitialContentPolicy(SlingInitialContentPolicy.EXTRACT_AND_REMOVE);
+        handler.handle("/jcr_root/apps/gav/install/composum-nodes-config-2.5.3.jar", archive, entry, converter);
+        // verify no additional content package created (as it only contains the configuration which should end up in the feature model only)
+        Mockito.verify(converter, Mockito.never()).processContentPackageArchive(Mockito.any(), Mockito.isNull());
+        // modified bundle
+        Mockito.verify(featuresManager).addArtifact(null, ArtifactId.fromMvnId("com.composum.nodes:composum-nodes-config:2.5.3-cp2fm-converted"), null);
+        // need to use ArgumentCaptur to properly compare string arrays
+        Mockito.verify(featuresManager).addConfiguration(ArgumentMatchers.isNull(), ArgumentMatchers.eq("org.apache.sling.jcr.base.internal.LoginAdminWhitelist.fragment~composum_core_v2"), ArgumentMatchers.eq("/jcr_root/libs/composum/nodes/install/org.apache.sling.jcr.base.internal.LoginAdminWhitelist.fragment-composum_core_v2.config"), dictionaryCaptor.capture());
+        assertEquals("composum_core", dictionaryCaptor.getValue().get("whitelist.name"));
+        assertArrayEquals(new String[] {
+                "com.composum.nodes.commons",
+                "com.composum.nodes.pckgmgr",
+                "com.composum.nodes.pckginstall" }, (String[])dictionaryCaptor.getValue().get("whitelist.bundles"));
+    }
+
+    @Test
+    public void testSlingInitialContentContainingConfigurationExtractAndKeep() throws Exception {
+        setUpArchive("/jcr_root/apps/gav/install/composum-nodes-config-2.5.3.jar", "composum-nodes-config-2.5.3.jar");
+        DefaultEntryHandlersManager handlersManager = new DefaultEntryHandlersManager();
+        converter.setEntryHandlersManager(handlersManager);
+        handler.setSlingInitialContentPolicy(SlingInitialContentPolicy.EXTRACT_AND_KEEP);
+        handler.handle("/jcr_root/apps/gav/install/composum-nodes-config-2.5.3.jar", archive, entry, converter);
+        // original bundle
+        Mockito.verify(featuresManager).addArtifact(null, ArtifactId.fromMvnId("com.composum.nodes:composum-nodes-config:2.5.3"), null);
+        // need to use ArgumentCaptur to properly compare string arrays
+        Mockito.verify(featuresManager).addConfiguration(ArgumentMatchers.isNull(), ArgumentMatchers.eq("org.apache.sling.jcr.base.internal.LoginAdminWhitelist.fragment~composum_core_v2"), ArgumentMatchers.eq("/jcr_root/libs/composum/nodes/install/org.apache.sling.jcr.base.internal.LoginAdminWhitelist.fragment-composum_core_v2.config"), dictionaryCaptor.capture());
+        assertEquals("composum_core", dictionaryCaptor.getValue().get("whitelist.name"));
+        assertArrayEquals(new String[] {
+                "com.composum.nodes.commons",
+                "com.composum.nodes.pckgmgr",
+                "com.composum.nodes.pckginstall" }, (String[])dictionaryCaptor.getValue().get("whitelist.bundles"));
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandlerGAVTest.java b/src/test/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandlerGAVTest.java
index 8b457a4..f83b353 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandlerGAVTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandlerGAVTest.java
@@ -16,72 +16,42 @@
  */
 package org.apache.sling.feature.cpconverter.handlers;
 
-import org.apache.jackrabbit.vault.fs.io.Archive;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.Mockito.when;
+
 import org.apache.sling.feature.ArtifactId;
-import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
-import org.apache.sling.feature.cpconverter.artifacts.ArtifactsDeployer;
-import org.apache.sling.feature.cpconverter.features.FeaturesManager;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
 
-import static org.junit.Assert.assertThrows;
-import static org.mockito.Mockito.when;
-
-public class BundleEntryHandlerGAVTest {
-    private final BundleEntryHandler handler = new BundleEntryHandler();
+@RunWith(MockitoJUnitRunner.class)
+public class BundleEntryHandlerGAVTest extends AbstractBundleEntryHandlerTest {
 
     @Test
-    public void testGAVwithProperties() throws Exception {
-        String path = "/jcr_root/apps/gav/install/core-1.0.0-SNAPSHOT.jar";
-        Archive.Entry entry = Mockito.mock(Archive.Entry.class);
-        when(entry.getName()).thenReturn(path);
-        Archive archive = Mockito.mock(Archive.class);
-        when(archive.openInputStream(entry)).thenReturn(BundleEntryHandlerGAVTest.class.getResourceAsStream("core-1.0.0-SNAPSHOT.jar"));
-        ContentPackage2FeatureModelConverter converter = Mockito.mock(ContentPackage2FeatureModelConverter.class);
-        ArtifactsDeployer deployer = Mockito.spy(ArtifactsDeployer.class);
-        when(converter.getArtifactsDeployer()).thenReturn(deployer);
-        FeaturesManager manager = Mockito.spy(FeaturesManager.class);
-        when(converter.getFeaturesManager()).thenReturn(manager);
-        handler.handle(path, archive, entry, converter);
-        Mockito.verify(manager).addArtifact(null, ArtifactId.fromMvnId("com.madplanet.sling.cp2sf:core:1.0.0-SNAPSHOT"),null);
+    public void testGAV() throws Exception {
+        setUpArchive("/jcr_root/apps/gav/install/core-1.0.0-SNAPSHOT.jar", "core-1.0.0-SNAPSHOT.jar");
+        handler.handle("/jcr_root/apps/gav/install/core-1.0.0-SNAPSHOT.jar", archive, entry, converter);
+        Mockito.verify(featuresManager).addArtifact(null, ArtifactId.fromMvnId("com.madplanet.sling.cp2sf:core:1.0.0-SNAPSHOT"), null);
     }
 
     @Test
     public void testGAVwithPom() throws Exception{
-        String path = "/jcr_root/apps/gav/install/org.osgi.service.jdbc-1.0.0.jar";
-        Archive.Entry entry = Mockito.mock(Archive.Entry.class);
-        when(entry.getName()).thenReturn(path);
-        Archive archive = Mockito.mock(Archive.class);
-        when(archive.openInputStream(entry)).thenReturn(BundleEntryHandlerGAVTest.class.getResourceAsStream("org.osgi.service.jdbc-1.0.0.jar"));
-        ContentPackage2FeatureModelConverter converter = Mockito.mock(ContentPackage2FeatureModelConverter.class);
-        ArtifactsDeployer deployer = Mockito.spy(ArtifactsDeployer.class);
-        when(converter.getArtifactsDeployer()).thenReturn(deployer);
-        FeaturesManager manager = Mockito.spy(FeaturesManager.class);
-        when(converter.getFeaturesManager()).thenReturn(manager);
-        handler.handle(path, archive, entry, converter);
-        Mockito.verify(manager).addArtifact(null, ArtifactId.fromMvnId("org.osgi:org.osgi.service.jdbc:1.0.0"),null);
+        setUpArchive("/jcr_root/apps/gav/install/org.osgi.service.jdbc-1.0.0.jar", "org.osgi.service.jdbc-1.0.0.jar");
+        handler.handle("/jcr_root/apps/gav/install/org.osgi.service.jdbc-1.0.0.jar", archive, entry, converter);
+        Mockito.verify(featuresManager).addArtifact(null, ArtifactId.fromMvnId("org.osgi:org.osgi.service.jdbc:1.0.0"), null);
     }
 
     @Test
     public void testNoGAV() throws Exception {
-        String path = "/jcr_root/apps/gav/install/org.osgi.service.jdbc-1.0.0-nogav.jar";
-        Archive.Entry entry = Mockito.mock(Archive.Entry.class);
-        when(entry.getName()).thenReturn(path);
-        Archive archive = Mockito.mock(Archive.class);
-        when(archive.openInputStream(entry)).thenReturn(BundleEntryHandlerGAVTest.class.getResourceAsStream("org.osgi.service.jdbc-1.0.0-nogav.jar"));
-        ContentPackage2FeatureModelConverter converter = Mockito.mock(ContentPackage2FeatureModelConverter.class);
-        ArtifactsDeployer deployer = Mockito.spy(ArtifactsDeployer.class);
-        when(converter.getArtifactsDeployer()).thenReturn(deployer);
-        FeaturesManager manager = Mockito.spy(FeaturesManager.class);
-        when(converter.getFeaturesManager()).thenReturn(manager);
-        handler.handle(path, archive, entry, converter);
-        Mockito.verify(manager).addArtifact(null, ArtifactId.fromMvnId("org.osgi.service:jdbc:1.0.0-201505202023"),null);
+        setUpArchive("/jcr_root/apps/gav/install/org.osgi.service.jdbc-1.0.0-nogav.jar", "org.osgi.service.jdbc-1.0.0-nogav.jar");
+        handler.handle("/jcr_root/apps/gav/install/org.osgi.service.jdbc-1.0.0-nogav.jar", archive, entry, converter);
+        Mockito.verify(featuresManager).addArtifact(null, ArtifactId.fromMvnId("org.osgi.service:jdbc:1.0.0-201505202023"), null);
     }
 
     @Test
     public void testBundleBelowConfigFolderWithEnforcement() throws Exception {
         handler.setEnforceBundlesBelowInstallFolder(true);
-        Archive.Entry entry = Mockito.mock(Archive.Entry.class);
         when(entry.getName()).thenReturn("mybundle.jar");
         assertThrows(IllegalStateException.class, () -> { handler.handle("/jcr_root/apps/myapp/config/mybundle.jar", null, entry, null); });
     }
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandlerTest.java b/src/test/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandlerTest.java
index 1174fe5..6c254de 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandlerTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/handlers/BundleEntryHandlerTest.java
@@ -82,11 +82,12 @@ public final class BundleEntryHandlerTest {
 
     private void deleteDirTree(File dir) throws IOException {
         Path tempDir = dir.toPath();
-
-        Files.walk(tempDir)
-            .sorted(Comparator.reverseOrder())
-            .map(Path::toFile)
-            .forEach(File::delete);
+        if (Files.exists(tempDir)) {
+            Files.walk(tempDir)
+                .sorted(Comparator.reverseOrder())
+                .map(Path::toFile)
+                .forEach(File::delete);
+        }
     }
 
     @Test
@@ -112,12 +113,14 @@ public final class BundleEntryHandlerTest {
 
         ContentPackage2FeatureModelConverter converter = mock(ContentPackage2FeatureModelConverter.class);
 
-        File testDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + '_' + System.currentTimeMillis());
+        File testDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + "_tst_" + System.currentTimeMillis());
+        File tmpDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + "_tmp_" + System.currentTimeMillis());
+        tmpDirectory.mkdirs();
         try {
 
             when(converter.getArtifactsDeployer()).thenReturn(new LocalMavenRepositoryArtifactsDeployer(testDirectory));
             when(converter.getFeaturesManager()).thenReturn(featuresManager);
-
+            when(converter.getTempDirectory()).thenReturn(tmpDirectory);
             bundleEntryHandler.handle(bundleLocation, archive, entry, converter);
 
             assertTrue(new File(testDirectory, "org/apache/felix/org.apache.felix.framework/6.0.1/org.apache.felix.framework-6.0.1.pom").exists());
@@ -129,6 +132,7 @@ public final class BundleEntryHandlerTest {
             assertEquals(startOrder, feature.getBundles().get(0).getStartOrder());
         } finally {
             deleteDirTree(testDirectory);
+            deleteDirTree(tmpDirectory);
         }
     }
 
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/handlers/GavDeclarationsInBundleTest.java b/src/test/java/org/apache/sling/feature/cpconverter/handlers/GavDeclarationsInBundleTest.java
index 7d766fc..237f51a 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/handlers/GavDeclarationsInBundleTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/handlers/GavDeclarationsInBundleTest.java
@@ -17,11 +17,11 @@
 package org.apache.sling.feature.cpconverter.handlers;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 
-import java.util.Properties;
-import java.util.jar.JarInputStream;
+import java.io.File;
+import java.util.jar.JarFile;
 
+import org.apache.sling.feature.ArtifactId;
 import org.junit.Test;
 
 public class GavDeclarationsInBundleTest {
@@ -29,6 +29,7 @@ public class GavDeclarationsInBundleTest {
     @Test
     public void guessRightGavsWhenMultiplePomPropertiesDeclarationsAreIncluded() throws Exception {
         verifyBundle("core-1.0.0-SNAPSHOT.jar",
+                     "core-1.0.0-SNAPSHOT",
                      "com.madplanet.sling.cp2sf",
                      "core",
                      "1.0.0-SNAPSHOT",
@@ -39,27 +40,24 @@ public class GavDeclarationsInBundleTest {
     @Test
     public void guessTheClassifierFromBundleName() throws Exception {
         verifyBundle("core-1.0.0-SNAPSHOT-classified.jar",
+                     "core-1.0.0-SNAPSHOT-classified",
                      "com.madplanet.sling.cp2sf",
                      "core",
                      "1.0.0-SNAPSHOT",
-                    "classified");
+                     "classified");
     }
 
-    private void verifyBundle(String bundleName,
+    private void verifyBundle(String resourceName,
+                              String bundleName,
                               String expectedGroupId,
                               String expectedArtifactId,
                               String expectedVersion,
                               String expectedClassifier) throws Exception {
         BundleEntryHandler bundleEntryHandler = new BundleEntryHandler();
 
-        try (JarInputStream jarInput = new JarInputStream(getClass().getResourceAsStream(bundleName))) {
-            Properties actual = bundleEntryHandler.readGav(bundleName, jarInput);
-
-            assertFalse(actual.isEmpty());
-            assertEquals(expectedGroupId, actual.getProperty("groupId"));
-            assertEquals(expectedArtifactId, actual.getProperty("artifactId"));
-            assertEquals(expectedVersion, actual.getProperty("version"));
-            assertEquals(expectedClassifier, actual.getProperty("classifier"));
+        try (JarFile jarFile = new JarFile(new File(getClass().getResource(resourceName).toURI()))) {
+            ArtifactId artifactId = bundleEntryHandler.extractArtifactId(bundleName, jarFile);
+            assertEquals(new ArtifactId(expectedGroupId, expectedArtifactId, expectedVersion, expectedClassifier, "jar"), artifactId);
         }
     }
 
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/vltpkg/VaultPackageAssemblerTest.java b/src/test/java/org/apache/sling/feature/cpconverter/vltpkg/VaultPackageAssemblerTest.java
index 6b67d6f..76b6ffc 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/vltpkg/VaultPackageAssemblerTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/vltpkg/VaultPackageAssemblerTest.java
@@ -102,7 +102,7 @@ public class VaultPackageAssemblerTest {
 
         VaultPackageAssembler assembler = VaultPackageAssembler.create(testDirectory, vaultPackage, false);
         PackageId packageId = vaultPackage.getId();
-        String fileName = packageId.toString().replaceAll("/", "-").replaceAll(":", "-") + "-" + vaultPackage.getFile().getName();
+        String fileName = packageId.toString().replaceAll("/", "-").replaceAll(":", "-");
         File storingDirectory = new File(assembler.getTempDir(), fileName + "-deflated");
         assertTrue("Storing Directory for Vault Package does not exist", storingDirectory.exists());
     }
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/composum-nodes-config-2.5.3.jar b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/composum-nodes-config-2.5.3.jar
new file mode 100644
index 0000000..1df1264
Binary files /dev/null and b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/composum-nodes-config-2.5.3.jar differ
diff --git a/src/test/resources/org/apache/sling/feature/cpconverter/handlers/io.wcm.handler.media-1.11.6.jar b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/io.wcm.handler.media-1.11.6.jar
new file mode 100644
index 0000000..8c04626
Binary files /dev/null and b/src/test/resources/org/apache/sling/feature/cpconverter/handlers/io.wcm.handler.media-1.11.6.jar differ