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/14 08:35:26 UTC

[sling-org-apache-sling-feature-cpconverter] branch master updated: SLING-10127 optionally emit converted content type packages to dedicated folder (#70)

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 bacfbd7  SLING-10127 optionally emit converted content type packages to dedicated folder (#70)
bacfbd7 is described below

commit bacfbd74fad08d1e48d3e813091aac0ded62c7b3
Author: Konrad Windszus <kw...@apache.org>
AuthorDate: Wed Apr 14 10:35:17 2021 +0200

    SLING-10127 optionally emit converted content type packages to dedicated folder (#70)
---
 README.md                                          |  86 ++++++++++++---
 .../ContentPackage2FeatureModelConverter.java      |  56 ++++++++--
 .../cpconverter/artifacts/ArtifactsDeployer.java   |   2 +-
 ... => LocalMavenRepositoryArtifactsDeployer.java} |  21 ++--
 .../artifacts/SimpleFolderArtifactsDeployer.java   |  61 +++++++++++
 ...ntentPackage2FeatureModelConverterLauncher.java |  23 +++-
 .../ContentPackage2FeatureModelConverterTest.java  | 121 ++++++++++++++++++---
 .../artifacts/DefaultBundlesDeployerTest.java      |   4 +-
 .../handlers/BundleEntryHandlerTest.java           |   4 +-
 9 files changed, 308 insertions(+), 70 deletions(-)

diff --git a/README.md b/README.md
index 66adecf..71907f2 100644
--- a/README.md
+++ b/README.md
@@ -6,11 +6,11 @@ This tool aims to provide to Apache Sling users an easy-to-use conversion tool w
 
 ## Introduction
 
-`content-package`s are zipped archives containing OSGi bundles, OSGi configurations and resources (and nested `content-package`s as well), aside metadata, that can be used to install content into a _JCR_ repository using the [Apache Jackrabbit FileVault](http://jackrabbit.apache.org/filevault/) packaging runtime.
+`content-package`s are zipped archives containing OSGi bundles, OSGi configurations, JCR nodes/properties and nested `content-package`s as well, aside [metadata](http://jackrabbit.apache.org/filevault/metadata.html), that can be used to install content into a _JCR_ repository using the [Apache Jackrabbit FileVault](http://jackrabbit.apache.org/filevault/) packaging runtime.
 
 OTOH, [Apache Sling Feature](https://github.com/apache/sling-org-apache-sling-feature) allows users to describe an entire OSGi-based application based on reusable components and includes everything related to this application, including bundles, configuration, framework properties, capabilities, requirements and custom artifacts.
 
-The _Apache Sling Content Package to Feature Model converter_ (referred as _cp2fm_) is a tool able to extract OSGI bundles, OSGi configurations, resources and iteratively scan nested `content-package`s from an input `content-package` and create one (or more) _Apache Sling Feature_ model files and deploy the extracted OSGi bundles in a directory which structure is compliant the _Apache Maven_ repository conventions.
+The _Apache Sling Content Package to Feature Model converter_ (referred as _cp2fm_) is a tool able to extract OSGI bundles, OSGi configurations and iteratively scan nested `content-package`s from an input `content-package` and create one (or more) _Apache Sling Feature_ model files and deploy the extracted OSGi bundles in a directory which structure is compliant the _Apache Maven_ repository conventions. The remaining JCR nodes/properties are kept in content packages which are either ref [...]
 
 ## Understanding the Input
 
@@ -147,9 +147,14 @@ Archive:  test-content.zip
     39481                     22 files
 ```
 
+### Package Types
+
+`content-package`s have one of [four package types](http://jackrabbit.apache.org/filevault/packagetypes.html).
+By default type `content` is never referenced inside the feature model (to work with Oak Composite Node Stores), while `application` and `mixed` type's are referenced in the generated feature models. `container` packages are dissolved and all sub packages are flattened (i.e. extracted as individual packages).
+
 ## Mapping and the Output
 
-All metadata are mainly collected inside one or more, depending by declared run modes in the installation and configuration paths, _Feature_ model files:
+All metadata are mainly collected inside one or more _Feature_ model files, depending on declared run modes in the installation and configuration paths:
 
 ```json
 $ cat asd.retail.all.json 
@@ -201,7 +206,9 @@ $ cat asd.retail.all-publish.json
 }
 ```
 
-### Binaries
+### OSGi Bundles and Content Packages
+
+All nodes and properties which are not OSGi configurations or bundles are kept in (stripped) content packages. Usually those content packages are referenced inside the Feature Model (evaluated by [Content Handler](https://github.com/apache/sling-org-apache-sling-feature-extension-content/blob/master/src/main/java/org/apache/sling/feature/extension/content/ContentHandler.java)). For packages of type `content` the user can define what to do with those packages (reference in feature model,  [...]
 
 All detected bundles are collected in an _Apache Maven repository_ compliant directory, all other resources are collected in a new `content-package`, usually classified as `cp2fm-converted-feature`, created while scanning the packages, which contains _content only_.
 
@@ -232,7 +239,7 @@ artifacts/
 12 directories, 8 files
 ```
 
-### Supported configurations
+### OSGi Configurations
 
 All OSGi configuration formats are supported:
 
@@ -316,7 +323,7 @@ production=org.apache.test.mytest-sample-site.ui.apps-production.json,org.apache
 
 The [org.apache.sling.feature.cpconverter.artifacts.ArtifactsDeployer](./src/main/java/org/apache/sling/feature/cpconverter/artifacts/ArtifactsDeployer.java) service is designed to let the conversion tool be integrated in external services, i.e. _Apache Maven_.
 
-The [default implementation](./src/main/java/org/apache/sling/feature/cpconverter/artifacts/DefaultArtifactsDeployer.java) just copies bundles in the target output directory, according to the _Apache Maven_ repository layout.
+The [default implementation](./src/main/java/org/apache/sling/feature/cpconverter/artifacts/LocalMavenRepositoryArtifactsDeployer.java) just copies bundles in the target output directory, according to the _Apache Maven_ repository layout.
 
 Bundles are collected in an _Apache Maven repository_ compliant directory, all other resources are collected in a new `content-package` created while scanning the packages:
 
@@ -421,7 +428,7 @@ The `!` character is used to separate nested sub-content-packages path.
 
 ## The CLI Tool
 
-The tool is distributed with a commodity package containing all is needed in order to launch the `ContentPackage2FeatureModelConverter` form the shell:
+The tool is distributed with a commodity package containing all is needed in order to launch the `ContentPackage2FeatureModelConverter` from the shell:
 
 ```bash
 $ unzip -l org.apache.sling.feature.cpconverter-0.0.1-SNAPSHOT.zip 
@@ -484,13 +491,52 @@ once the package is decompressed, open the shell and type:
 
 ```bash
 $ ./bin/cp2fm -h
-Usage: cp2fm [-hmqsvX] -a=<artifactsOutputDirectory> [-b=<bundlesStartOrder>]
-             [-i=<artifactIdOverride>] -o=<featureModelsOutputDirectory>
-             [-p=<fmPrefix>] [-D=<String=String>]...
-             [-f=<filteringPatterns>]... [-r=<apiRegions>]...
-             content-packages...
+Usage: cp2fm [-hmqsvXZ] [--disable-installer-policy]
+             [--enforce-servicemapping-by-principal] [--remove-install-hooks]
+             [--content-type-package-policy=<contentTypePackagePolicy>]
+             [--enforce-principal-based-supported-path=<enforcePrincipalBasedSup
+             portedPath>] [--seed-feature=<seedFeature>]
+             [--system-user-rel-path=<systemUserRelPath>]
+             -a=<artifactsOutputDirectory> [-b=<bundlesStartOrder>]
+             [-e=<exportsToRegion>] [-i=<artifactIdOverride>]
+             -o=<featureModelsOutputDirectory> [-p=<fmPrefix>]
+             -u=<unreferencedArtifactsOutputDirectory>
+             [--entry-handler-config=<entryHandlerConfigs>]...
+             [-D=<String=String>]... [-f=<filteringPatterns>]...
+             [-r=<apiRegions>]... content-packages...
 Apache Sling Content Package to Sling Feature converter
       content-packages...   The content-package input file(s).
+      --content-type-package-policy=<contentTypePackagePolicy>
+                            Determines what to do with converted packages of type
+                              'content'. Valid values: REFERENCE, DROP,
+                              PUT_IN_DEDICATED_FOLDER.
+                              Default: DROP
+      --disable-installer-policy
+                            Disables enforcing that OSGi configurations are only
+                              allowed below a folder called 'config' and OSGi
+                              bundles are only allowed below a folder called
+                              'install'. Instead both are detected below either
+                              'install' or 'config'.
+      --enforce-principal-based-supported-path=<enforcePrincipalBasedSupportedPath>
+                            Converts service user access control entries to
+                              principal-based setup using the given supported path.
+      --enforce-servicemapping-by-principal
+                            Converts service user mappings with the form 'service:
+                              sub=userID' to 'service:sub=[principalname]'. Note,
+                              this may result in group membership no longer being
+                              resolved upon service login.
+      --entry-handler-config=<entryHandlerConfigs>
+                            Config for entry handlers that support it (classname:
+                              <config-string>
+      --remove-install-hooks
+                            Removes both internal and external hooks from processed
+                              packages
+      --seed-feature=<seedFeature>
+                            A url pointing to a feature that can be assumed to be
+                              around when the conversion result will be used
+      --system-user-rel-path=<systemUserRelPath>
+                            Relative path for system user as configured with Apache
+                              Jackrabbit Oak
   -a, --artifacts-output-directory=<artifactsOutputDirectory>
                             The output directory where the artifacts will be
                               deployed.
@@ -498,6 +544,9 @@ Apache Sling Content Package to Sling Feature converter
                             The order to start detected bundles.
   -D, --define=<String=String>
                             Define a system property
+  -e, --exports-to-region=<exportsToRegion>
+                            Packages exported by bundles in the content packages are
+                              exported in the named region
   -f, --filtering-patterns=<filteringPatterns>
                             Regex based pattern(s) to reject content-package archive
                               entries.
@@ -518,15 +567,16 @@ Apache Sling Content Package to Sling Feature converter
                             The API Regions assigned to the generated features
   -s, --strict-validation   Flag to mark the content-package input file being strict
                               validated.
+  -u, --unreferenced-artifacts-output-directory=<unreferencedArtifactsOutputDirectory
+        >
+                            The output directory where unreferenced artifacts will
+                              be deployed.
   -v, --version             Display version information.
   -X, --verbose             Produce execution debug output.
   -Z, --fail-on-mixed-packages
                             Fail the conversion if the resulting attached
-                              content-package is MIXED type.
-  --enforce-principal-based-supported-path=<path>
-                            Converts service user access control entries to principal-based 
-                              setup using the given supported path.
-Copyright(c) 2019 The Apache Software Foundation.
+                              content-package is MIXED type
+Copyright(c) 2019-2021 The Apache Software Foundation.
 ```
 
 to see all the available options; a sample execution could look like:
@@ -551,7 +601,7 @@ then execute the command
 
 ```bash
 > ./bin/cp2fm @argfile
-````
+```
 
 ## Failures and Restrictions
 
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 0a277b1..49cb21e 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
@@ -85,6 +85,8 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
 
     private ArtifactsDeployer artifactsDeployer;
 
+    private ArtifactsDeployer unreferencedArtifactsDeployer;
+
     private VaultPackageAssembler mainPackageAssembler;
 
     private final RecollectorVaultPackageScanner recollectorVaultPackageScanner;
@@ -93,7 +95,18 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
 
     private boolean failOnMixedPackages = false;
 
-    private boolean dropContent = false;
+    public enum PackagePolicy {
+        /** References the content package in the feature model and deploys via the {@link ContentPackage2FeatureModelConverter#artifactsDeployer} */
+        REFERENCE, 
+        /** Drops the content package completely (i.e. neither reference it in the feature model nor deploy anywhere)
+          * @deprecated
+          */
+        DROP,
+        /** Deploys the content package via the {@link ContentPackage2FeatureModelConverter#unreferencedArtifactsDeployer} */
+        PUT_IN_DEDICATED_FOLDER;
+    }
+
+    private PackagePolicy contentTypePackagePolicy = PackagePolicy.REFERENCE;
 
     private boolean removeInstallHooks = false;
 
@@ -144,6 +157,11 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
         return this;
     }
 
+    public @NotNull ContentPackage2FeatureModelConverter setUnreferencedArtifactsDeployer(@Nullable ArtifactsDeployer unreferencedArtifactsDeployer) {
+        this.unreferencedArtifactsDeployer = unreferencedArtifactsDeployer;
+        return this;
+    }
+
     public @Nullable AclManager getAclManager() {
         return aclManager;
     }
@@ -162,8 +180,8 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
         return this;
     }
     
-    public @NotNull ContentPackage2FeatureModelConverter setDropContent(boolean dropContent) {
-        this.dropContent = dropContent;
+    public @NotNull ContentPackage2FeatureModelConverter setContentTypePackagePolicy(PackagePolicy contentTypePackagePolicy) {
+        this.contentTypePackagePolicy = contentTypePackagePolicy;
         return this;
     }
 
@@ -370,15 +388,33 @@ public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanne
                                     + " is of MIXED type");
             }
 
-            // don't deploy & add content-packages of type content to featuremodel if dropContent is set
-            if (PackageType.CONTENT != packageType || !dropContent) {
-                // deploy the new content-package to the local mvn bundles dir and attach it to the feature
+            
+            // special handling for converted packages of type content
+            if (PackageType.CONTENT == packageType) {
+                switch (contentTypePackagePolicy) {
+                    case DROP:
+                        mutableContentsIds.put(originalPackageId, getDependencies(vaultPackage));
+                        logger.info("Dropping package of PackageType.CONTENT {} (content-package id: {})",
+                                    mvnPackageId.getArtifactId(), originalPackageId);
+                        break;
+                    case PUT_IN_DEDICATED_FOLDER:
+                        mutableContentsIds.put(originalPackageId, 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());
+                        break;
+                    case REFERENCE:
+                        artifactsDeployer.deploy(new FileArtifactWriter(contentPackageArchive), mvnPackageId);
+                        featuresManager.addArtifact(runMode, mvnPackageId);
+                }
+            } else {
+                // deploy the new content-package to the local mvn bundles dir
                 artifactsDeployer.deploy(new FileArtifactWriter(contentPackageArchive), mvnPackageId);
                 featuresManager.addArtifact(runMode, mvnPackageId);
-            } else {
-                mutableContentsIds.put(originalPackageId, getDependencies(vaultPackage));
-                logger.info("Dropping package of PackageType.CONTENT {} (content-package id: {})",
-                            mvnPackageId.getArtifactId(), originalPackageId);
             }
         }
     }
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/artifacts/ArtifactsDeployer.java b/src/main/java/org/apache/sling/feature/cpconverter/artifacts/ArtifactsDeployer.java
index c94dfe9..b498945 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/artifacts/ArtifactsDeployer.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/artifacts/ArtifactsDeployer.java
@@ -24,7 +24,7 @@ import org.jetbrains.annotations.NotNull;
 
 public interface ArtifactsDeployer {
 
-    @NotNull File getBundlesDirectory();
+    @NotNull File getBaseDirectory();
 
     void deploy(@NotNull ArtifactWriter artifactWriter, @NotNull ArtifactId id) throws IOException;
 
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/artifacts/DefaultArtifactsDeployer.java b/src/main/java/org/apache/sling/feature/cpconverter/artifacts/LocalMavenRepositoryArtifactsDeployer.java
similarity index 82%
rename from src/main/java/org/apache/sling/feature/cpconverter/artifacts/DefaultArtifactsDeployer.java
rename to src/main/java/org/apache/sling/feature/cpconverter/artifacts/LocalMavenRepositoryArtifactsDeployer.java
index eaec649..3ddd1d9 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/artifacts/DefaultArtifactsDeployer.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/artifacts/LocalMavenRepositoryArtifactsDeployer.java
@@ -28,13 +28,16 @@ import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public final class DefaultArtifactsDeployer implements ArtifactsDeployer {
+/**
+ * Stores the deployed artifacts in a <a href="https://cwiki.apache.org/confluence/display/MAVENOLD/Repository+Layout+-+Final">local Maven repository layout</a>.
+ */
+public final class LocalMavenRepositoryArtifactsDeployer implements ArtifactsDeployer {
 
     private final Logger logger = LoggerFactory.getLogger(getClass());
 
     private final File artifactsDirectory;
 
-    public DefaultArtifactsDeployer(@NotNull File outputDirectory) {
+    public LocalMavenRepositoryArtifactsDeployer(@NotNull File outputDirectory) {
         artifactsDirectory = outputDirectory;
         if (!artifactsDirectory.exists()) {
             artifactsDirectory.mkdirs();
@@ -42,7 +45,7 @@ public final class DefaultArtifactsDeployer implements ArtifactsDeployer {
     }
 
     @Override
-    public @NotNull File getBundlesDirectory() {
+    public @NotNull File getBaseDirectory() {
         return artifactsDirectory;
     }
 
@@ -65,18 +68,8 @@ public final class DefaultArtifactsDeployer implements ArtifactsDeployer {
 
         // deploy the main artifact
 
-        StringBuilder nameBuilder = new StringBuilder()
-                                    .append(id.getArtifactId())
-                                    .append('-')
-                                    .append(id.getVersion());
-
-        if (id.getClassifier() != null) {
-            nameBuilder.append('-').append(id.getClassifier());
-        }
-
-        nameBuilder.append('.').append(id.getType());
 
-        File targetFile = new File(targetDir, nameBuilder.toString());
+        File targetFile = new File(targetDir, id.toMvnName());
 
         logger.info("Writing data to {}...", targetFile);
 
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/artifacts/SimpleFolderArtifactsDeployer.java b/src/main/java/org/apache/sling/feature/cpconverter/artifacts/SimpleFolderArtifactsDeployer.java
new file mode 100644
index 0000000..4afb626
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/artifacts/SimpleFolderArtifactsDeployer.java
@@ -0,0 +1,61 @@
+/*
+ * 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.artifacts;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import org.apache.sling.feature.ArtifactId;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Stores the deployed artifacts as files in a flat folder structure.
+ * All deployed artifact ids must be unique among all group ids to prevent overwriting files.
+ */
+public class SimpleFolderArtifactsDeployer implements ArtifactsDeployer {
+
+    private final File artifactsDirectory;
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    public SimpleFolderArtifactsDeployer(@NotNull File outputDirectory) {
+        artifactsDirectory = outputDirectory;
+        if (!artifactsDirectory.exists()) {
+            artifactsDirectory.mkdirs();
+        }
+    }
+
+    @Override
+    public @NotNull File getBaseDirectory() {
+        return artifactsDirectory;
+    }
+
+    @Override
+    public void deploy(@NotNull ArtifactWriter artifactWriter, @NotNull ArtifactId id) throws IOException {
+        File targetFile = new File(artifactsDirectory, id.toMvnName());
+        logger.info("Writing data to {}...", targetFile);
+
+        try (FileOutputStream targetStream = new FileOutputStream(targetFile)) {
+            artifactWriter.write(targetStream);
+        }
+
+        logger.info("Data successfully written to {}.", targetFile);
+    }
+
+}
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 6fe8ca0..2503076 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
@@ -31,7 +31,8 @@ import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
 import org.apache.sling.feature.cpconverter.accesscontrol.AclManager;
 import org.apache.sling.feature.cpconverter.accesscontrol.DefaultAclManager;
-import org.apache.sling.feature.cpconverter.artifacts.DefaultArtifactsDeployer;
+import org.apache.sling.feature.cpconverter.artifacts.LocalMavenRepositoryArtifactsDeployer;
+import org.apache.sling.feature.cpconverter.artifacts.SimpleFolderArtifactsDeployer;
 import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager;
 import org.apache.sling.feature.cpconverter.filtering.RegexBasedResourceFilter;
 import org.apache.sling.feature.cpconverter.handlers.DefaultEntryHandlersManager;
@@ -43,13 +44,14 @@ import org.slf4j.LoggerFactory;
 
 import picocli.CommandLine;
 import picocli.CommandLine.Command;
+import picocli.CommandLine.Help.Visibility;
 import picocli.CommandLine.Option;
 import picocli.CommandLine.Parameters;
 
 @Command(
     name = "cp2fm",
     description = "Apache Sling Content Package to Sling Feature converter",
-    footer = "Copyright(c) 2019 The Apache Software Foundation."
+    footer = "Copyright(c) 2019-2021 The Apache Software Foundation."
 )
 public final class ContentPackage2FeatureModelConverterLauncher implements Runnable {
 
@@ -125,6 +127,12 @@ public final class ContentPackage2FeatureModelConverterLauncher implements Runna
     @Option(names = { "--disable-installer-policy" }, description = "Disables enforcing that OSGi configurations are only allowed below a folder called 'config' and OSGi bundles are only allowed below a folder called 'install'. Instead both are detected below either 'install' or 'config'.", required = false)
     private boolean disableInstallerPolicy = false;
 
+    @Option(names = { "--content-type-package-policy" }, description = "Determines what to do with converted packages of type 'content'. Valid values: ${COMPLETION-CANDIDATES}.", required = false, showDefaultValue = Visibility.ALWAYS)
+    private ContentPackage2FeatureModelConverter.PackagePolicy contentTypePackagePolicy = ContentPackage2FeatureModelConverter.PackagePolicy.DROP;
+
+    @Option(names = { "-u", "--unreferenced-artifacts-output-directory" }, description = "The output directory where unreferenced artifacts will be deployed.", required = true)
+    private File unreferencedArtifactsOutputDirectory;
+
     @Override
     public void run() {
         if (quiet) {
@@ -195,13 +203,18 @@ public final class ContentPackage2FeatureModelConverterLauncher implements Runna
 
                 ContentPackage2FeatureModelConverter converter = new ContentPackage2FeatureModelConverter(strictValidation)
                                                                 .setFeaturesManager(featuresManager)
-                                                                .setBundlesDeployer(new DefaultArtifactsDeployer(artifactsOutputDirectory))
+                                                                .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(artifactsOutputDirectory))
                                                                 .setEntryHandlersManager(new DefaultEntryHandlersManager(entryHandlerConfigsMap, !disableInstallerPolicy))
                                                                 .setAclManager(aclManager)
                                                                 .setEmitter(DefaultPackagesEventsEmitter.open(featureModelsOutputDirectory))
                                                                 .setFailOnMixedPackages(failOnMixedPackages)
-                                                                .setDropContent(true);
-
+                                                                .setContentTypePackagePolicy(contentTypePackagePolicy);
+                                                                
+                if (unreferencedArtifactsOutputDirectory != null) {
+                    converter.setUnreferencedArtifactsDeployer(new SimpleFolderArtifactsDeployer(unreferencedArtifactsOutputDirectory));
+                } else if (contentTypePackagePolicy == ContentPackage2FeatureModelConverter.PackagePolicy.PUT_IN_DEDICATED_FOLDER) {
+                    throw new IllegalStateException("Argument '--content-type-package-policy PUT_IN_DEDICATED_FOLDER' requires argument '--unreferenced-artifacts-output-directory' as well!");
+                }
                 try {
                     if (filteringPatterns != null && filteringPatterns.length > 0) {
                         RegexBasedResourceFilter filter = new RegexBasedResourceFilter();
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java b/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
index 5cc4f2d..2fb623e 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
@@ -55,8 +55,10 @@ import org.apache.sling.feature.Configuration;
 import org.apache.sling.feature.Extension;
 import org.apache.sling.feature.ExtensionType;
 import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter.PackagePolicy;
 import org.apache.sling.feature.cpconverter.accesscontrol.DefaultAclManager;
-import org.apache.sling.feature.cpconverter.artifacts.DefaultArtifactsDeployer;
+import org.apache.sling.feature.cpconverter.artifacts.LocalMavenRepositoryArtifactsDeployer;
+import org.apache.sling.feature.cpconverter.artifacts.SimpleFolderArtifactsDeployer;
 import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager;
 import org.apache.sling.feature.cpconverter.filtering.RegexBasedResourceFilter;
 import org.apache.sling.feature.cpconverter.handlers.DefaultEntryHandlersManager;
@@ -141,7 +143,7 @@ public class ContentPackage2FeatureModelConverterTest {
         try {
 
             converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null, new DefaultAclManager()))
-                    .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                    .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
                     .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                     .convert(packageFile);
 
@@ -212,7 +214,7 @@ public class ContentPackage2FeatureModelConverterTest {
     }
 
     @Test
-    public void convertContentPackageDropContent() throws Exception {
+    public void convertContentPackageDropContentTypePackagePolicy() throws Exception {
         URL packageUrl = getClass().getResource("test-content-package.zip");
         File packageFile = FileUtils.toFile(packageUrl);
 
@@ -221,9 +223,9 @@ public class ContentPackage2FeatureModelConverterTest {
         try {
 
             converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null, new DefaultAclManager()))
-                    .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                    .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
                     .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
-                    .setDropContent(true)
+                    .setContentTypePackagePolicy(PackagePolicy.DROP)
                     .convert(packageFile);
 
             verifyFeatureFile(outputDirectory,
@@ -282,6 +284,89 @@ public class ContentPackage2FeatureModelConverterTest {
     }
 
     @Test
+    public void convertContentPackagePutInDedicatedFolderContentTypePackagePolicy() throws Exception {
+        URL packageUrl = getClass().getResource("test-content-package.zip");
+        File packageFile = FileUtils.toFile(packageUrl);
+
+        File outputDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + '_' + System.currentTimeMillis());
+        File outputDirectoryUnreferencedArtifacts = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + "_unreferenced_" + System.currentTimeMillis());
+
+        try {
+
+            converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null, new DefaultAclManager()))
+                    .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
+                    .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
+                    .setContentTypePackagePolicy(PackagePolicy.PUT_IN_DEDICATED_FOLDER)
+                    .setUnreferencedArtifactsDeployer(new SimpleFolderArtifactsDeployer(outputDirectoryUnreferencedArtifacts))
+                    .convert(packageFile);
+
+            verifyFeatureFile(outputDirectory,
+                            "asd.retail.all.json",
+                            "asd.sample:asd.retail.all:slingosgifeature:0.0.1",
+                            Arrays.asList("org.apache.felix:org.apache.felix.framework:6.0.1"),
+                            Arrays.asList("org.apache.sling.commons.log.LogManager.factory.config~asd-retail"),
+                            Arrays.asList("asd.sample:asd.retail.apps:zip:cp2fm-converted:0.0.1",
+                                    "asd:Asd.Retail.config:zip:cp2fm-converted:0.0.1"));
+            verifyFeatureFile(outputDirectory,
+                            "asd.retail.all-author.json",
+                            "asd.sample:asd.retail.all:slingosgifeature:author:0.0.1",
+                            Arrays.asList("org.apache.sling:org.apache.sling.api:2.20.0"),
+                            Collections.emptyList(),
+                            Collections.emptyList());
+            verifyFeatureFile(outputDirectory,
+                            "asd.retail.all-publish.json",
+                            "asd.sample:asd.retail.all:slingosgifeature:publish:0.0.1",
+                            Arrays.asList("org.apache.sling:org.apache.sling.models.api:1.3.8"),
+                            Arrays.asList("org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~asd-retail"),
+                            Collections.emptyList());
+
+            // verify the runmode.mapper integrity
+            File runmodeMapperFile = new File(outputDirectory, "runmode.mapping");
+            assertTrue(runmodeMapperFile.exists());
+            assertTrue(runmodeMapperFile.isFile());
+            Properties runModes = new Properties();
+            try (FileInputStream input = new FileInputStream(runmodeMapperFile)) {
+                runModes.load(input);
+            }
+            assertFalse(runModes.isEmpty());
+            assertTrue(runModes.containsKey("(default)"));
+            assertEquals("asd.retail.all.json", runModes.getProperty("(default)"));
+            assertEquals("asd.retail.all-author.json", runModes.getProperty("author"));
+            assertEquals("asd.retail.all-publish.json", runModes.getProperty("publish"));
+
+            verifyContentPackage(new File(outputDirectory, "asd/Asd.Retail.config/0.0.1/Asd.Retail.config-0.0.1-cp2fm-converted.zip"),
+                    "META-INF/vault/settings.xml",
+                    "META-INF/vault/properties.xml",
+                    "META-INF/vault/config.xml",
+                    "META-INF/vault/filter.xml",
+                    "jcr_root/apps/.content.xml");
+            verifyContentPackage(new File(outputDirectoryUnreferencedArtifacts, "Asd.Retail.ui.content-0.0.1-cp2fm-converted.zip"),
+                    "META-INF/vault/settings.xml",
+                    "META-INF/vault/properties.xml",
+                    "META-INF/vault/config.xml",
+                    "META-INF/vault/filter.xml",
+                    "META-INF/vault/filter-plugin-generated.xml",
+                    "jcr_root/content/asd/.content.xml",
+                    "jcr_root/content/asd/resources.xml");
+            verifyContentPackage(new File(outputDirectory, "asd/sample/asd.retail.apps/0.0.1/asd.retail.apps-0.0.1-cp2fm-converted.zip"),
+                    "META-INF/vault/settings.xml",
+                    "META-INF/vault/properties.xml",
+                    "META-INF/vault/config.xml",
+                    "META-INF/vault/filter.xml",
+                    "META-INF/vault/filter-plugin-generated.xml");
+            verifyContentPackage(new File(outputDirectoryUnreferencedArtifacts, "asd.retail.all-0.0.1-cp2fm-converted.zip"),
+                    "META-INF/vault/settings.xml",
+                    "META-INF/vault/properties.xml",
+                    "META-INF/vault/config.xml",
+                    "META-INF/vault/filter.xml");
+
+        } finally {
+            deleteDirTree(outputDirectory);
+            deleteDirTree(outputDirectoryUnreferencedArtifacts);
+        }
+    }
+
+    @Test
     public void convertContentPackageRemoveInstallHooks() throws Exception {
         URL packageUrl = getClass().getResource("test-with-install-hooks.zip");
         File packageFile = FileUtils.toFile(packageUrl);
@@ -290,7 +375,7 @@ public class ContentPackage2FeatureModelConverterTest {
 
         try {
             converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null, new DefaultAclManager()))
-                    .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                    .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
                     .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                     .setRemoveInstallHooks(true)
                     .convert(packageFile);
@@ -318,7 +403,7 @@ public class ContentPackage2FeatureModelConverterTest {
 
         try {
             converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null, new DefaultAclManager()))
-                    .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                    .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
                     .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                     .convert(packageFile);
 
@@ -347,7 +432,7 @@ public class ContentPackage2FeatureModelConverterTest {
             DefaultFeaturesManager fm = new DefaultFeaturesManager(true, 5, outDir, null, null, null, new DefaultAclManager());
             fm.setAPIRegions(Arrays.asList("global", "foo.bar"));
             converter.setFeaturesManager(fm)
-                     .setBundlesDeployer(new DefaultArtifactsDeployer(outDir))
+                     .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outDir))
                      .setEmitter(DefaultPackagesEventsEmitter.open(outDir))
                      .convert(cpFile);
 
@@ -510,7 +595,7 @@ public class ContentPackage2FeatureModelConverterTest {
         try {
 
             converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null, new DefaultAclManager()))
-                    .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                    .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
                     .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                     .convert(packageFile);
         } finally {
@@ -532,7 +617,7 @@ public class ContentPackage2FeatureModelConverterTest {
         try {
 
             converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null, new DefaultAclManager()))
-                    .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                    .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
                     .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                     .convert(packageFile);
             
@@ -563,7 +648,7 @@ public class ContentPackage2FeatureModelConverterTest {
             URL packageUrl = getClass().getResource("test-content-package.zip");
             File packageFile = FileUtils.toFile(packageUrl);
     
-            converter.setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+            converter.setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
                      .setFeaturesManager(new DefaultFeaturesManager(DefaultFeaturesManager.ConfigurationHandling.STRICT, 5, outputDirectory, null, null, null, new DefaultAclManager()))
                      .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                      .convert(packageFile);
@@ -584,7 +669,7 @@ public class ContentPackage2FeatureModelConverterTest {
             URL packageUrl = getClass().getResource("test-content-package.zip");
             File packageFile = FileUtils.toFile(packageUrl);
     
-            converter.setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+            converter.setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
                      .setFeaturesManager(new DefaultFeaturesManager(false, 5, outputDirectory, null, null, null, new DefaultAclManager()))
                      .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                      .convert(packageFile);
@@ -608,7 +693,7 @@ public class ContentPackage2FeatureModelConverterTest {
             URL packageUrl = getClass().getResource("test-content-package.zip");
             File packageFile = FileUtils.toFile(packageUrl);
     
-            converter.setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+            converter.setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
                      .setFeaturesManager(new DefaultFeaturesManager(false, 5, outputDirectory, null, null, null, new DefaultAclManager()))
                      .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                      .convert(packageFile);
@@ -633,7 +718,7 @@ public class ContentPackage2FeatureModelConverterTest {
         File outDir = Files.createTempDirectory(getClass().getSimpleName()).toFile();
 
         try {
-            converter.setBundlesDeployer(new DefaultArtifactsDeployer(outDir))
+            converter.setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outDir))
             .setFeaturesManager(new DefaultFeaturesManager(false, 5, outDir, null, null, null, new DefaultAclManager())
                     .setAPIRegions(Arrays.asList("a.b.c")))
             .setEmitter(DefaultPackagesEventsEmitter.open(outDir))
@@ -676,7 +761,7 @@ public class ContentPackage2FeatureModelConverterTest {
 
             String overrideId = "${project.groupId}:${project.artifactId}:slingosgifeature:asd.test.all-1.0.0:${project.version}";
             converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, overrideId, null, null, new DefaultAclManager()))
-                    .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                    .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
                     .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                     .convert(packageFile);
 
@@ -749,7 +834,7 @@ public class ContentPackage2FeatureModelConverterTest {
         try {
 
             converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null, new DefaultAclManager()))
-                    .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                    .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
                     .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                     .convert(contentPackages[0]);
 
@@ -794,8 +879,8 @@ public class ContentPackage2FeatureModelConverterTest {
         File outputDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + '_' + System.currentTimeMillis());
         try {
             converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null, new DefaultAclManager()))
-                    .setDropContent(true)
-                    .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                    .setContentTypePackagePolicy(PackagePolicy.DROP)
+                    .setBundlesDeployer(new LocalMavenRepositoryArtifactsDeployer(outputDirectory))
                     .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                     .convert(contentPackages);
 
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/artifacts/DefaultBundlesDeployerTest.java b/src/test/java/org/apache/sling/feature/cpconverter/artifacts/DefaultBundlesDeployerTest.java
index a7505be..0fba49c 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/artifacts/DefaultBundlesDeployerTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/artifacts/DefaultBundlesDeployerTest.java
@@ -39,7 +39,7 @@ public class DefaultBundlesDeployerTest {
     @Before
     public void setUp() {
         outputDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + '_' + System.currentTimeMillis());
-        artifactDeployer = new DefaultArtifactsDeployer(outputDirectory);
+        artifactDeployer = new LocalMavenRepositoryArtifactsDeployer(outputDirectory);
     }
 
     @After
@@ -57,7 +57,7 @@ public class DefaultBundlesDeployerTest {
 
     @Test
     public void verifyBundlesDirectory() {
-        File bundlesDirectory = artifactDeployer.getBundlesDirectory();
+        File bundlesDirectory = artifactDeployer.getBaseDirectory();
         assertNotNull(bundlesDirectory);
         assertTrue(bundlesDirectory.exists());
         assertTrue(bundlesDirectory.isDirectory());
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 28d1e86..1174fe5 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
@@ -40,7 +40,7 @@ import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
 import org.apache.sling.feature.ArtifactId;
 import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
-import org.apache.sling.feature.cpconverter.artifacts.DefaultArtifactsDeployer;
+import org.apache.sling.feature.cpconverter.artifacts.LocalMavenRepositoryArtifactsDeployer;
 import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager;
 import org.apache.sling.feature.cpconverter.features.FeaturesManager;
 import org.junit.Test;
@@ -115,7 +115,7 @@ public final class BundleEntryHandlerTest {
         File testDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + '_' + System.currentTimeMillis());
         try {
 
-            when(converter.getArtifactsDeployer()).thenReturn(new DefaultArtifactsDeployer(testDirectory));
+            when(converter.getArtifactsDeployer()).thenReturn(new LocalMavenRepositoryArtifactsDeployer(testDirectory));
             when(converter.getFeaturesManager()).thenReturn(featuresManager);
 
             bundleEntryHandler.handle(bundleLocation, archive, entry, converter);