You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by si...@apache.org on 2019/05/08 01:15:05 UTC

[sling-org-apache-sling-feature-cpconverter] branch master updated: all Features management stuff moved to a dedicated package

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

simonetripodi 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 1a39015  all Features management stuff moved to a dedicated package
1a39015 is described below

commit 1a39015f0c31aa52f447871bac61b7c526cc2186
Author: stripodi <st...@simos-mbp>
AuthorDate: Wed May 8 03:14:57 2019 +0200

    all Features management stuff moved to a dedicated package
---
 .../ContentPackage2FeatureModelConverter.java      | 326 +++------------------
 ...ntentPackage2FeatureModelConverterLauncher.java |  16 +-
 .../features/DefaultFeaturesManager.java           | 242 +++++++++++++++
 .../cpconverter/features/FeaturesManager.java      |  45 +++
 .../cpconverter/{ => features}/RunmodeMapper.java  |   2 +-
 .../feature/cpconverter/features/package-info.java |  21 ++
 .../AbstractConfigurationEntryHandler.java         |   2 +-
 .../cpconverter/handlers/BundleEntryHandler.java   |  12 +-
 .../ContentPackage2FeatureModelConverterTest.java  |  58 +---
 .../cpconverter/features/FeaturesManagerTest.java  |  67 +++++
 .../handlers/BundleEntryHandlerTest.java           |  17 +-
 .../handlers/ConfigEntryHandlerTest.java           |  13 +-
 .../handlers/ConfigurationEntryHandlerTest.java    |  14 +-
 .../handlers/RepPolicyEntryHandlerTest.java        |   6 +-
 .../handlers/SystemUsersEntryHandlerTest.java      |   6 +-
 15 files changed, 486 insertions(+), 361 deletions(-)

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 397a8e9..a043a27 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
@@ -19,13 +19,7 @@ package org.apache.sling.feature.cpconverter;
 import static java.util.Objects.requireNonNull;
 
 import java.io.File;
-import java.io.FileWriter;
-import java.util.Collection;
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.HashMap;
 import java.util.Iterator;
-import java.util.Map;
 import java.util.ServiceLoader;
 
 import org.apache.jackrabbit.vault.fs.io.Archive;
@@ -34,118 +28,60 @@ import org.apache.jackrabbit.vault.packaging.PackageManager;
 import org.apache.jackrabbit.vault.packaging.PackageProperties;
 import org.apache.jackrabbit.vault.packaging.VaultPackage;
 import org.apache.jackrabbit.vault.packaging.impl.PackageManagerImpl;
-import org.apache.sling.feature.Artifact;
-import org.apache.sling.feature.ArtifactId;
-import org.apache.sling.feature.Artifacts;
-import org.apache.sling.feature.Configuration;
-import org.apache.sling.feature.Extension;
-import org.apache.sling.feature.ExtensionType;
-import org.apache.sling.feature.Extensions;
-import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.cpconverter.acl.AclManager;
 import org.apache.sling.feature.cpconverter.artifacts.ArtifactsDeployer;
 import org.apache.sling.feature.cpconverter.artifacts.FileArtifactWriter;
+import org.apache.sling.feature.cpconverter.features.FeaturesManager;
 import org.apache.sling.feature.cpconverter.filtering.ResourceFilter;
-import org.apache.sling.feature.cpconverter.interpolator.SimpleVariablesInterpolator;
-import org.apache.sling.feature.cpconverter.interpolator.VariablesInterpolator;
 import org.apache.sling.feature.cpconverter.spi.EntryHandler;
 import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageAssembler;
-import org.apache.sling.feature.io.json.FeatureJSONWriter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class ContentPackage2FeatureModelConverter {
 
-    private static final String CONTENT_PACKAGES = "content-packages";
-
     public static final String ZIP_TYPE = "zip";
 
     public static final String PACKAGE_CLASSIFIER = "cp2fm-converted";
 
-    private static final String SLING_OSGI_FEATURE_TILE_TYPE = "slingosgifeature";
-
-    private static final String JSON_FILE_EXTENSION = ".json";
-
     private static final String DEFEAULT_VERSION = "0.0.0";
 
     private final Logger logger = LoggerFactory.getLogger(getClass());
 
     private final PackageManager packageManager = new PackageManagerImpl();
 
-    private final ServiceLoader<EntryHandler> entryHandlers = ServiceLoader.load(EntryHandler.class);
+    private final boolean strictValidation;
 
-    private final Map<String, Feature> runModes = new HashMap<>();
+    private final ServiceLoader<EntryHandler> entryHandlers = ServiceLoader.load(EntryHandler.class);
 
     private final AclManager aclManager = new AclManager();
 
-    private final VariablesInterpolator interpolator = new SimpleVariablesInterpolator();
+    private FeaturesManager featuresManager;
 
     private ResourceFilter resourceFilter;
 
     private ArtifactsDeployer artifactsDeployer;
 
-    private boolean strictValidation = false;
-
-    private boolean mergeConfigurations = false;
-
-    private int bundlesStartOrder = 0;
-
-    private File featureModelsOutputDirectory;
-
-    private Feature targetFeature = null;
-
     private VaultPackageAssembler mainPackageAssembler = null;
 
-    private String id;
-
-    private String idOverride;
-
-    private Map<String, String> properties;
+    public ContentPackage2FeatureModelConverter() {
+        this(false);
+    }
 
-    public ContentPackage2FeatureModelConverter setStrictValidation(boolean strictValidation) {
+    public ContentPackage2FeatureModelConverter(boolean strictValidation) {
         this.strictValidation = strictValidation;
-        return this;
     }
 
     public boolean isStrictValidation() {
         return strictValidation;
     }
 
-    public boolean isMergeConfigurations() {
-        return mergeConfigurations;
-    }
-
-    public ContentPackage2FeatureModelConverter setMergeConfigurations(boolean mergeConfigurations) {
-        this.mergeConfigurations = mergeConfigurations;
-        return this;
-    }
-
-    public ContentPackage2FeatureModelConverter setBundlesStartOrder(int bundlesStartOrder) {
-        this.bundlesStartOrder = bundlesStartOrder;
-        return this;
-    }
-
-    public ContentPackage2FeatureModelConverter setFeatureModelsOutputDirectory(File featureModelsOutputDirectory) {
-        this.featureModelsOutputDirectory = featureModelsOutputDirectory;
-        return this;
-    }
-
-    public Feature getTargetFeature() {
-        return targetFeature;
+    public FeaturesManager getFeaturesManager() {
+        return featuresManager;
     }
 
-    public ContentPackage2FeatureModelConverter setId(String id) {
-        this.id = id;
-        return this;
-    }
-
-    public ContentPackage2FeatureModelConverter setIdOverride(String id) {
-        this.idOverride = id;
-        return this;
-    }
-
-    public ContentPackage2FeatureModelConverter setProperties(Map<String, String> properties) {
-        this.properties = properties;
+    public ContentPackage2FeatureModelConverter setFeaturesManager(FeaturesManager featuresManager) {
+        this.featuresManager = featuresManager;
         return this;
     }
 
@@ -171,51 +107,6 @@ public class ContentPackage2FeatureModelConverter {
         return mainPackageAssembler;
     }
 
-    public Feature getRunMode(String runMode) {
-        if (getTargetFeature() == null) {
-            throw new IllegalStateException("Target Feature not initialized yet, please make sure convert() method was invoked first.");
-        }
-
-        if (runMode == null) {
-            return getTargetFeature();
-        }
-
-        ArtifactId newId = appendRunmode(getTargetFeature().getId(), runMode);
-
-        return runModes.computeIfAbsent(runMode, k -> new Feature(newId));
-    }
-
-    private ArtifactId appendRunmode(ArtifactId id, String runMode) {
-        ArtifactId newId;
-        if (runMode == null) {
-            newId = id;
-        } else {
-            final String classifier;
-            if (id.getClassifier() != null && !id.getClassifier().isEmpty()) {
-                classifier = id.getClassifier() + '-' + runMode;
-            } else {
-                classifier = runMode;
-            }
-
-            newId = new ArtifactId(id.getGroupId(), id.getArtifactId(), id.getVersion(), classifier, id.getType());
-        }
-        return newId;
-    }
-
-    private static void checkDirectory(File directory, String name) {
-        if (directory == null) {
-            throw new IllegalStateException("Null " + name + " output directory not supported, it must be set before invoking the convert(File) method.");
-        }
-
-        if (!directory.exists() && !directory.mkdirs()) {
-            throw new IllegalStateException("output directory "
-                                            + directory
-                                            + " does not exist and can not be created, please make sure current user '"
-                                            + System.getProperty("user.name")
-                                            + " has enough rights to write on the File System.");
-        }
-    }
-
     public void convert(File contentPackage) throws Exception {
         requireNonNull(contentPackage , "Null content-package can not be converted.");
 
@@ -225,8 +116,6 @@ public class ContentPackage2FeatureModelConverter {
                                             + " does not exist or it is not a valid file.");
         }
 
-        checkDirectory(featureModelsOutputDirectory, "models");
-
         logger.info("Reading content-package '{}'...", contentPackage);
 
         try (VaultPackage vaultPackage = packageManager.open(contentPackage, strictValidation)) {
@@ -235,40 +124,32 @@ public class ContentPackage2FeatureModelConverter {
             mainPackageAssembler = VaultPackageAssembler.create(vaultPackage);
             PackageProperties packageProperties = vaultPackage.getProperties();
 
-            ArtifactId artifactId;
-            if (id != null && !id.isEmpty()) {
-                artifactId = ArtifactId.fromMvnId(id);
-            } else {
-                String group = requireNonNull(packageProperties.getProperty(PackageProperties.NAME_GROUP),
-                                              PackageProperties.NAME_GROUP
-                                              + " property not found in content-package "
-                                              + contentPackage
-                                              + ", please check META-INF/vault/properties.xml")
-                               .replace('/', '.');
-
-                String name = requireNonNull(packageProperties.getProperty(PackageProperties.NAME_NAME),
-                                             PackageProperties.NAME_NAME
-                                             + " property not found in content-package "
-                                             + contentPackage
-                                             + ", please check META-INF/vault/properties.xml");
-
-                String version = packageProperties.getProperty(PackageProperties.NAME_VERSION);
-                if (version == null || version.isEmpty()) {
-                    version = DEFEAULT_VERSION;
-                }
-
-                artifactId = new ArtifactId(group,
-                                            name,
-                                            version,
-                                            null,
-                                            SLING_OSGI_FEATURE_TILE_TYPE);
+            String group = requireNonNull(packageProperties.getProperty(PackageProperties.NAME_GROUP),
+                                          PackageProperties.NAME_GROUP
+                                          + " property not found in content-package "
+                                          + contentPackage
+                                          + ", please check META-INF/vault/properties.xml")
+                                          .replace('/', '.');
+
+            String name = requireNonNull(packageProperties.getProperty(PackageProperties.NAME_NAME),
+                                        PackageProperties.NAME_NAME
+                                        + " property not found in content-package "
+                                        + contentPackage
+                                        + ", please check META-INF/vault/properties.xml");
+
+            String version = packageProperties.getProperty(PackageProperties.NAME_VERSION);
+            if (version == null || version.isEmpty()) {
+                version = DEFEAULT_VERSION;
             }
 
-            targetFeature = new Feature(artifactId);
+            String description = packageProperties.getDescription();
 
-            targetFeature.setDescription(packageProperties.getDescription());
+            featuresManager.init(group,
+                                 name,
+                                 version,
+                                 description);
 
-            logger.info("Converting content-package '{}' to Feature File '{}'...", vaultPackage.getId(), targetFeature.getId());
+            logger.info("Converting content-package '{}'...", vaultPackage.getId());
 
             process(vaultPackage);
 
@@ -279,104 +160,31 @@ public class ContentPackage2FeatureModelConverter {
             // deploy the new zip content-package to the local mvn bundles dir
 
             artifactsDeployer.deploy(new FileArtifactWriter(contentPackageArchive),
-                                   targetFeature.getId().getGroupId(),
-                                   targetFeature.getId().getArtifactId(),
-                                   targetFeature.getId().getVersion(),
-                                   PACKAGE_CLASSIFIER,
-                                   ZIP_TYPE);
-
-            attach(null,
-                   targetFeature.getId().getGroupId(),
-                   targetFeature.getId().getArtifactId(),
-                   targetFeature.getId().getVersion(),
-                   PACKAGE_CLASSIFIER,
-                   ZIP_TYPE);
+                                     featuresManager.getTargetFeature().getId().getGroupId(),
+                                     featuresManager.getTargetFeature().getId().getArtifactId(),
+                                     featuresManager.getTargetFeature().getId().getVersion(),
+                                     PACKAGE_CLASSIFIER,
+                                     ZIP_TYPE);
+
+            featuresManager.addArtifact(null,
+                                        featuresManager.getTargetFeature().getId().getGroupId(),
+                                        featuresManager.getTargetFeature().getId().getArtifactId(),
+                                        featuresManager.getTargetFeature().getId().getVersion(),
+                                        PACKAGE_CLASSIFIER,
+                                        ZIP_TYPE);
 
             // finally serialize the Feature Model(s) file(s)
 
-            aclManager.addRepoinitExtension(mainPackageAssembler, getTargetFeature());
+            aclManager.addRepoinitExtension(mainPackageAssembler, featuresManager.getTargetFeature());
 
             logger.info("Conversion complete!");
 
-            RunmodeMapper runmodeMapper = RunmodeMapper.open(featureModelsOutputDirectory);
-
-            seralize(getTargetFeature(), null, runmodeMapper);
+            featuresManager.serialize();
 
-            if (!runModes.isEmpty()) {
-                for (java.util.Map.Entry<String, Feature> runmodeEntry : runModes.entrySet()) {
-                    String runmode = runmodeEntry.getKey();
-                    seralize(runmodeEntry.getValue(), runmode, runmodeMapper);
-                }
-            }
-
-            runmodeMapper.save();
             aclManager.reset();
         }
     }
 
-    public void addConfiguration(String runMode, String pid, Dictionary<String, Object> configurationProperties) {
-        Feature feature = getRunMode(runMode);
-        Configuration configuration = feature.getConfigurations().getConfiguration(pid);
-
-        if (configuration == null) {
-            configuration = new Configuration(pid);
-            feature.getConfigurations().add(configuration);
-        } else if (!mergeConfigurations) {
-            throw new IllegalStateException("Configuration '"
-                                            + pid
-                                            + "' already defined in Feature Model '"
-                                            + feature.getId().toMvnId()
-                                            + "', set the 'mergeConfigurations' flag to 'true' if you want to merge multiple configurations with same PID");
-        }
-
-        Enumeration<String> keys = configurationProperties.keys();
-        while (keys.hasMoreElements()) {
-            String key = keys.nextElement();
-            Object value = configurationProperties.get(key);
-
-            if (value != null && Collection.class.isInstance(value)) {
-                value = ((Collection<?>) value).toArray();
-            }
-
-            configuration.getProperties().put(key, value);
-        }
-    }
-
-    private void seralize(Feature feature, String runMode, RunmodeMapper runmodeMapper) throws Exception {
-        StringBuilder fileNameBuilder = new StringBuilder().append(feature.getId().getArtifactId());
-
-        String classifier = feature.getId().getClassifier();
-        if (classifier != null && !classifier.isEmpty()) {
-            fileNameBuilder.append('-').append(classifier);
-        }
-
-        if (properties != null) {
-            properties.put("filename", fileNameBuilder.toString());
-        }
-
-        fileNameBuilder.append(JSON_FILE_EXTENSION);
-
-        String fileName = fileNameBuilder.toString();
-
-        File targetFile = new File(featureModelsOutputDirectory, fileName);
-
-        logger.info("Writing resulting Feature Model '{}' to file '{}'...", feature.getId(), targetFile);
-
-        if (idOverride != null && !idOverride.isEmpty()) {
-            String interpolatedIdOverride = interpolator.interpolate(idOverride, properties);
-            ArtifactId idOverrride = appendRunmode(ArtifactId.parse(interpolatedIdOverride), runMode);
-            feature = feature.copy(idOverrride);
-        }
-
-        try (FileWriter targetWriter = new FileWriter(targetFile)) {
-            FeatureJSONWriter.write(targetWriter, feature);
-
-            logger.info("'{}' Feature File successfully written!", targetFile);
-
-            runmodeMapper.addOrUpdate(runMode, fileName);
-        }
-    }
-
     public void processSubPackage(String path, File contentPackage) throws Exception {
         requireNonNull(path, "Impossible to process a null vault package");
         requireNonNull(contentPackage, "Impossible to process a null vault package");
@@ -397,10 +205,6 @@ public class ContentPackage2FeatureModelConverter {
     private void process(VaultPackage vaultPackage) throws Exception {
         requireNonNull(vaultPackage, "Impossible to process a null vault package");
 
-        if (getTargetFeature() == null) {
-            throw new IllegalStateException("Target Feature not initialized yet, please make sure convert() method was invoked first.");
-        }
-
         Archive archive = vaultPackage.getArchive();
         try {
             archive.open(strictValidation);
@@ -459,38 +263,4 @@ public class ContentPackage2FeatureModelConverter {
         return mainPackageAssembler;
     }
 
-    public void attach(String runMode,
-                       String groupId,
-                       String artifactId,
-                       String version,
-                       String classifier,
-                       String type) {
-        requireNonNull(groupId, "Artifact can not be attached to a feature without specifying a valid 'groupId'.");
-        requireNonNull(artifactId, "Artifact can not be attached to a feature without specifying a valid 'artifactId'.");
-        requireNonNull(version, "Artifact can not be attached to a feature without specifying a valid 'version'.");
-        requireNonNull(type, "Artifact can not be attached to a feature without specifying a valid 'type'.");
-
-        Artifact artifact = new Artifact(new ArtifactId(groupId, artifactId, version, classifier, type));
-
-        Feature targetFeature = getRunMode(runMode);
-        Artifacts artifacts;
-
-        if (ZIP_TYPE.equals(type) ) {
-            Extensions extensions = targetFeature.getExtensions();
-            Extension extension = extensions.getByName(CONTENT_PACKAGES);
-
-            if (extension == null) {
-                extension = new Extension(ExtensionType.ARTIFACTS, CONTENT_PACKAGES, true);
-                extensions.add(extension);
-            }
-
-            artifacts = extension.getArtifacts();
-        } else {
-            artifact.setStartOrder(bundlesStartOrder);
-            artifacts = targetFeature.getBundles();
-        }
-
-        artifacts.add(artifact);
-    }
-
 }
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 de25873..c8a4ae0 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
@@ -33,6 +33,7 @@ import org.apache.jackrabbit.vault.packaging.PackageId;
 import org.apache.jackrabbit.vault.packaging.impl.ZipVaultPackage;
 import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
 import org.apache.sling.feature.cpconverter.artifacts.DefaultArtifactsDeployer;
+import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager;
 import org.apache.sling.feature.cpconverter.filtering.RegexBasedResourceFilter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -80,7 +81,7 @@ public final class ContentPackage2FeatureModelConverterLauncher implements Runna
     private File featureModelsOutputDirectory;
 
     @Option(names = { "-i", "--artifact-id" }, description = "The optional Artifact Id the Feature File will have, once generated; it will be derived, if not specified.", required = false)
-    private String artifactId;
+    private String artifactIdOverride;
 
     @Option(names = {"-D", "--define"}, description = "Define a system property", required = false)
     private Map<String, String> properties = new HashMap<>();
@@ -116,13 +117,12 @@ public final class ContentPackage2FeatureModelConverterLauncher implements Runna
         logger.info("");
 
         try {
-            ContentPackage2FeatureModelConverter converter = new ContentPackage2FeatureModelConverter()
-                                                             .setStrictValidation(strictValidation)
-                                                             .setMergeConfigurations(mergeConfigurations)
-                                                             .setBundlesStartOrder(bundlesStartOrder)
-                                                             .setFeatureModelsOutputDirectory(featureModelsOutputDirectory)
-                                                             .setIdOverride(artifactId)
-                                                             .setProperties(properties)
+            ContentPackage2FeatureModelConverter converter = new ContentPackage2FeatureModelConverter(strictValidation)
+                                                             .setFeaturesManager(new DefaultFeaturesManager(mergeConfigurations,
+                                                                                                            bundlesStartOrder,
+                                                                                                            featureModelsOutputDirectory,
+                                                                                                            artifactIdOverride,
+                                                                                                            properties))
                                                              .setBundlesDeployer(new DefaultArtifactsDeployer(artifactsOutputDirectory));
 
             if (filteringPatterns != null && filteringPatterns.length > 0) {
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
new file mode 100644
index 0000000..2c484fc
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java
@@ -0,0 +1,242 @@
+/*
+ * 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.features;
+
+import static java.util.Objects.requireNonNull;
+import static org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter.ZIP_TYPE;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Artifacts;
+import org.apache.sling.feature.Configuration;
+import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
+import org.apache.sling.feature.Extensions;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.cpconverter.interpolator.SimpleVariablesInterpolator;
+import org.apache.sling.feature.cpconverter.interpolator.VariablesInterpolator;
+import org.apache.sling.feature.io.json.FeatureJSONWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DefaultFeaturesManager implements FeaturesManager {
+
+    private static final String CONTENT_PACKAGES = "content-packages";
+
+    private static final String SLING_OSGI_FEATURE_TILE_TYPE = "slingosgifeature";
+
+    private static final String JSON_FILE_EXTENSION = ".json";
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    private final Map<String, Feature> runModes = new HashMap<>();
+
+    private final VariablesInterpolator interpolator = new SimpleVariablesInterpolator();
+
+    private final boolean mergeConfigurations;
+
+    private final int bundlesStartOrder;
+
+    private final File featureModelsOutputDirectory;
+
+    private final String artifactIdOverride;
+
+    private final Map<String, String> properties;
+
+    private Feature targetFeature = null;
+
+    public DefaultFeaturesManager() {
+        this(true, 20, new File(System.getProperty("java.io.tmpdir")), null, null);
+    }
+
+    public DefaultFeaturesManager(boolean mergeConfigurations,
+                                  int bundlesStartOrder,
+                                  File featureModelsOutputDirectory,
+                                  String artifactIdOverride,
+                                  Map<String, String> properties) {
+        this.mergeConfigurations = mergeConfigurations;
+        this.bundlesStartOrder = bundlesStartOrder;
+        this.featureModelsOutputDirectory = featureModelsOutputDirectory;
+        this.artifactIdOverride = artifactIdOverride;
+        this.properties = properties;
+    }
+
+    public void init(String groupId,
+                     String artifactId,
+                     String version,
+                     String description) {
+        targetFeature = new Feature(new ArtifactId(groupId, artifactId, version, null, SLING_OSGI_FEATURE_TILE_TYPE));
+        targetFeature.setDescription(description);
+    }
+
+    public Feature getTargetFeature() {
+        return targetFeature;
+    }
+
+    public Feature getRunMode(String runMode) {
+        if (getTargetFeature() == null) {
+            throw new IllegalStateException("Target Feature not initialized yet, please make sure convert() method was invoked first.");
+        }
+
+        if (runMode == null) {
+            return getTargetFeature();
+        }
+
+        ArtifactId newId = appendRunmode(getTargetFeature().getId(), runMode);
+
+        return runModes.computeIfAbsent(runMode, k -> new Feature(newId));
+    }
+
+    public void addArtifact(String runMode,
+                            String groupId,
+                            String artifactId,
+                            String version,
+                            String classifier,
+                            String type) {
+        requireNonNull(groupId, "Artifact can not be attached to a feature without specifying a valid 'groupId'.");
+        requireNonNull(artifactId, "Artifact can not be attached to a feature without specifying a valid 'artifactId'.");
+        requireNonNull(version, "Artifact can not be attached to a feature without specifying a valid 'version'.");
+        requireNonNull(type, "Artifact can not be attached to a feature without specifying a valid 'type'.");
+
+        Artifact artifact = new Artifact(new ArtifactId(groupId, artifactId, version, classifier, type));
+
+        Feature targetFeature = getRunMode(runMode);
+        Artifacts artifacts;
+
+        if (ZIP_TYPE.equals(type) ) {
+            Extensions extensions = targetFeature.getExtensions();
+            Extension extension = extensions.getByName(CONTENT_PACKAGES);
+
+            if (extension == null) {
+                extension = new Extension(ExtensionType.ARTIFACTS, CONTENT_PACKAGES, true);
+                extensions.add(extension);
+            }
+
+            artifacts = extension.getArtifacts();
+        } else {
+            artifact.setStartOrder(bundlesStartOrder);
+            artifacts = targetFeature.getBundles();
+        }
+
+        artifacts.add(artifact);
+    }
+
+    private ArtifactId appendRunmode(ArtifactId id, String runMode) {
+        ArtifactId newId;
+        if (runMode == null) {
+            newId = id;
+        } else {
+            final String classifier;
+            if (id.getClassifier() != null && !id.getClassifier().isEmpty()) {
+                classifier = id.getClassifier() + '-' + runMode;
+            } else {
+                classifier = runMode;
+            }
+
+            newId = new ArtifactId(id.getGroupId(), id.getArtifactId(), id.getVersion(), classifier, id.getType());
+        }
+        return newId;
+    }
+
+    public void addConfiguration(String runMode, String pid, Dictionary<String, Object> configurationProperties) {
+        Feature feature = getRunMode(runMode);
+        Configuration configuration = feature.getConfigurations().getConfiguration(pid);
+
+        if (configuration == null) {
+            configuration = new Configuration(pid);
+            feature.getConfigurations().add(configuration);
+        } else if (!mergeConfigurations) {
+            throw new IllegalStateException("Configuration '"
+                                            + pid
+                                            + "' already defined in Feature Model '"
+                                            + feature.getId().toMvnId()
+                                            + "', set the 'mergeConfigurations' flag to 'true' if you want to merge multiple configurations with same PID");
+        }
+
+        Enumeration<String> keys = configurationProperties.keys();
+        while (keys.hasMoreElements()) {
+            String key = keys.nextElement();
+            Object value = configurationProperties.get(key);
+
+            if (value != null && Collection.class.isInstance(value)) {
+                value = ((Collection<?>) value).toArray();
+            }
+
+            configuration.getProperties().put(key, value);
+        }
+    }
+
+    public void serialize() throws Exception {
+        RunmodeMapper runmodeMapper = RunmodeMapper.open(featureModelsOutputDirectory);
+
+        seralize(targetFeature, null, runmodeMapper);
+
+        if (!runModes.isEmpty()) {
+            for (Entry<String, Feature> runmodeEntry : runModes.entrySet()) {
+                String runmode = runmodeEntry.getKey();
+                seralize(runmodeEntry.getValue(), runmode, runmodeMapper);
+            }
+        }
+
+        runmodeMapper.save();
+    }
+
+    private void seralize(Feature feature, String runMode, RunmodeMapper runmodeMapper) throws Exception {
+        StringBuilder fileNameBuilder = new StringBuilder().append(feature.getId().getArtifactId());
+
+        String classifier = feature.getId().getClassifier();
+        if (classifier != null && !classifier.isEmpty()) {
+            fileNameBuilder.append('-').append(classifier);
+        }
+
+        if (properties != null) {
+            properties.put("filename", fileNameBuilder.toString());
+        }
+
+        fileNameBuilder.append(JSON_FILE_EXTENSION);
+
+        String fileName = fileNameBuilder.toString();
+
+        File targetFile = new File(featureModelsOutputDirectory, fileName);
+
+        logger.info("Writing resulting Feature Model '{}' to file '{}'...", feature.getId(), targetFile);
+
+        if (artifactIdOverride != null && !artifactIdOverride.isEmpty()) {
+            String interpolatedIdOverride = interpolator.interpolate(artifactIdOverride, properties);
+            ArtifactId idOverrride = appendRunmode(ArtifactId.parse(interpolatedIdOverride), runMode);
+            feature = feature.copy(idOverrride);
+        }
+
+        try (FileWriter targetWriter = new FileWriter(targetFile)) {
+            FeatureJSONWriter.write(targetWriter, feature);
+
+            logger.info("'{}' Feature File successfully written!", targetFile);
+
+            runmodeMapper.addOrUpdate(runMode, fileName);
+        }
+    }
+
+}
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
new file mode 100644
index 0000000..6c2f8b5
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/features/FeaturesManager.java
@@ -0,0 +1,45 @@
+/*
+ * 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.features;
+
+import java.util.Dictionary;
+
+import org.apache.sling.feature.Feature;
+
+public interface FeaturesManager {
+
+    void init(String groupId,
+              String artifactId,
+              String version,
+              String description);
+
+    Feature getTargetFeature();
+
+    Feature getRunMode(String runMode);
+
+    void addArtifact(String runMode,
+                     String groupId,
+                     String artifactId,
+                     String version,
+                     String classifier,
+                     String type);
+
+    void addConfiguration(String runMode, String pid, Dictionary<String, Object> configurationProperties);
+
+    void serialize() throws Exception;
+
+}
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/RunmodeMapper.java b/src/main/java/org/apache/sling/feature/cpconverter/features/RunmodeMapper.java
similarity index 97%
rename from src/main/java/org/apache/sling/feature/cpconverter/RunmodeMapper.java
rename to src/main/java/org/apache/sling/feature/cpconverter/features/RunmodeMapper.java
index 01b0072..23d1347 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/RunmodeMapper.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/features/RunmodeMapper.java
@@ -14,7 +14,7 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.sling.feature.cpconverter;
+package org.apache.sling.feature.cpconverter.features;
 
 import java.io.File;
 import java.io.FileInputStream;
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/features/package-info.java b/src/main/java/org/apache/sling/feature/cpconverter/features/package-info.java
new file mode 100644
index 0000000..b6b7a2e
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/features/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+/**
+ * Features manager definition and default implementation.
+ */
+package org.apache.sling.feature.cpconverter.features;
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 49d348b..8216e48 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
@@ -77,7 +77,7 @@ abstract class AbstractConfigurationEntryHandler extends AbstractRegexEntryHandl
                                             + "' but it does not, currently");
         }
 
-        converter.addConfiguration(runMode, id, configurationProperties);
+        converter.getFeaturesManager().addConfiguration(runMode, id, configurationProperties);
     }
 
     protected abstract Dictionary<String, Object> parseConfiguration(String name, InputStream input) throws Exception;
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 922963f..1e7e3e6 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
@@ -106,12 +106,12 @@ public final class BundleEntryHandler extends AbstractRegexEntryHandler {
                                                   classifier,
                                                   JAR_TYPE);
 
-            converter.attach(runMode,
-                             groupId,
-                             artifactId,
-                             version,
-                             classifier,
-                             JAR_TYPE);
+            converter.getFeaturesManager().addArtifact(runMode,
+                                                       groupId,
+                                                       artifactId,
+                                                       version,
+                                                       classifier,
+                                                       JAR_TYPE);
         }
     }
 
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 bb7d988..efe8b13 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
@@ -20,7 +20,6 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.mock;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -39,6 +38,7 @@ import org.apache.commons.io.FileUtils;
 import org.apache.sling.feature.ArtifactId;
 import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.cpconverter.artifacts.DefaultArtifactsDeployer;
+import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager;
 import org.apache.sling.feature.cpconverter.filtering.RegexBasedResourceFilter;
 import org.apache.sling.feature.io.json.FeatureJSONReader;
 import org.junit.After;
@@ -75,51 +75,11 @@ public class ContentPackage2FeatureModelConverterTest {
         converter.convert(testDirectory);
     }
 
-    @Test(expected = IllegalStateException.class)
-    public void getRunModeRequiresConvertInvoked() {
-        converter.getRunMode(null);
-    }
-
-    @Test(expected = IllegalStateException.class)
-    public void addConfigurationRequiresConvertInvoked() {
-        converter.setMergeConfigurations(true).getRunMode(null);
-    }
-
     @Test(expected = NullPointerException.class)
     public void processRequiresNotNullPackage() throws Exception {
         converter.processSubPackage("", null);
     }
 
-    @Test(expected = IllegalStateException.class)
-    public void processRequiresConvertInvoked() throws Exception {
-        converter.processSubPackage("", mock(File.class));
-    }
-
-    @Test(expected = NullPointerException.class)
-    public void deployLocallyAndAttachRequiresNonNullInput() throws Exception {
-        converter.attach(null, null, null, null, null, null);
-    }
-
-    @Test(expected = NullPointerException.class)
-    public void deployLocallyAndAttachRequiresNonNullGroupId() throws Exception {
-        converter.attach(null, null, null, null, null, null);
-    }
-
-    @Test(expected = NullPointerException.class)
-    public void deployLocallyAndAttachRequiresNonNullArtifactId() throws Exception {
-        converter.attach(null, "org.apache.sling", null, null, null, null);
-    }
-
-    @Test(expected = NullPointerException.class)
-    public void deployLocallyAndAttachRequiresNonNullVersion() throws Exception {
-        converter.attach(null, "org.apache.sling", "org.apache.sling.cm2fm", null, null, null);
-    }
-
-    @Test(expected = NullPointerException.class)
-    public void deployLocallyAndAttachRequiresNonNullType() throws Exception {
-        converter.attach(null, "org.apache.sling", "org.apache.sling.cm2fm", "0.0.1", null, null);
-    }
-
     @Test
     public void convertContentPackage() throws Exception {
         URL packageUrl = getClass().getResource("test-content-package.zip");
@@ -127,9 +87,8 @@ public class ContentPackage2FeatureModelConverterTest {
 
         File outputDirectory = new File(System.getProperty("testDirectory"), getClass().getName() + '_' + System.currentTimeMillis());
 
-        converter.setBundlesStartOrder(5)
+        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null))
                  .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
-                 .setFeatureModelsOutputDirectory(outputDirectory)
                  .convert(packageFile);
 
         verifyFeatureFile(outputDirectory,
@@ -253,9 +212,8 @@ public class ContentPackage2FeatureModelConverterTest {
 
         File outputDirectory = new File(System.getProperty("testDirectory"), getClass().getName() + '_' + System.currentTimeMillis());
 
-        converter.setBundlesStartOrder(5)
+        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null))
                  .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
-                 .setFeatureModelsOutputDirectory(outputDirectory)
                  .convert(packageFile);
     }
 
@@ -275,12 +233,12 @@ public class ContentPackage2FeatureModelConverterTest {
         File packageFile = FileUtils.toFile(packageUrl);
 
         converter.setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
-                 .setFeatureModelsOutputDirectory(outputDirectory)
+                 .setFeaturesManager(new DefaultFeaturesManager(false, 5, outputDirectory, null, null))
                  .convert(packageFile);
 
         String pid = "this.is.just.a.pid";
-        converter.addConfiguration(runmodeA, pid, new Hashtable<String, Object>());
-        converter.addConfiguration(runmodeB, pid, new Hashtable<String, Object>());
+        converter.getFeaturesManager().addConfiguration(runmodeA, pid, new Hashtable<String, Object>());
+        converter.getFeaturesManager().addConfiguration(runmodeB, pid, new Hashtable<String, Object>());
     }
 
     @Test
@@ -291,10 +249,8 @@ public class ContentPackage2FeatureModelConverterTest {
         File outputDirectory = new File(System.getProperty("testDirectory"), getClass().getName() + '_' + System.currentTimeMillis());
 
         String overrideId = "${project.groupId}:${project.artifactId}:slingosgifeature:asd.test.all-1.0.0:${project.version}";
-        converter.setBundlesStartOrder(5)
+        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, overrideId, null))
                  .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
-                 .setFeatureModelsOutputDirectory(outputDirectory)
-                 .setIdOverride(overrideId)
                  .convert(packageFile);
 
         verifyFeatureFile(outputDirectory,
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/features/FeaturesManagerTest.java b/src/test/java/org/apache/sling/feature/cpconverter/features/FeaturesManagerTest.java
new file mode 100644
index 0000000..02c3882
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/cpconverter/features/FeaturesManagerTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.features;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class FeaturesManagerTest {
+
+    private FeaturesManager featuresManager;
+
+    @Before
+    public void setUp() {
+        featuresManager = new DefaultFeaturesManager();
+    }
+
+    @After
+    public void tearDown() {
+        featuresManager = null;
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void getRunModeRequiresConvertInvoked() {
+        featuresManager.getRunMode(null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void deployLocallyAndaddArtifactRequiresNonNullInput() throws Exception {
+        featuresManager.addArtifact(null, null, null, null, null, null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void deployLocallyAndaddArtifactRequiresNonNullGroupId() throws Exception {
+        featuresManager.addArtifact(null, null, null, null, null, null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void deployLocallyAndaddArtifactRequiresNonNullArtifactId() throws Exception {
+        featuresManager.addArtifact(null, "org.apache.sling", null, null, null, null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void deployLocallyAndaddArtifactRequiresNonNullVersion() throws Exception {
+        featuresManager.addArtifact(null, "org.apache.sling", "org.apache.sling.cm2fm", null, null, null);
+    }
+
+    @Test(expected = NullPointerException.class)
+    public void deployLocallyAndaddArtifactRequiresNonNullType() throws Exception {
+        featuresManager.addArtifact(null, "org.apache.sling", "org.apache.sling.cm2fm", "0.0.1", null, 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 bca7ce1..142890b 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
@@ -36,6 +36,8 @@ 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.features.DefaultFeaturesManager;
+import org.apache.sling.feature.cpconverter.features.FeaturesManager;
 import org.apache.sling.feature.cpconverter.spi.EntryHandler;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -86,23 +88,24 @@ public final class BundleEntryHandlerTest {
 
         });
 
+        Feature feature = new Feature(new ArtifactId("org.apache.sling", "org.apache.sling.cp2fm", "0.0.1", null, null));
+        FeaturesManager featuresManager = spy(DefaultFeaturesManager.class);
+        when(featuresManager.getTargetFeature()).thenReturn(feature);
+        when(featuresManager.getRunMode(anyString())).thenReturn(feature);
+        doCallRealMethod().when(featuresManager).addArtifact(anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
+
         ContentPackage2FeatureModelConverter converter = spy(ContentPackage2FeatureModelConverter.class);
 
         File testDirectory = new File(System.getProperty("testDirectory"), getClass().getName() + '_' + System.currentTimeMillis());
-
-        doCallRealMethod().when(converter).attach(anyString(), anyString(), anyString(), anyString(), anyString(), anyString());
         when(converter.getArtifactsDeployer()).thenReturn(new DefaultArtifactsDeployer(testDirectory));
-
-        Feature feature = new Feature(new ArtifactId("org.apache.sling", "org.apache.sling.cp2fm", "0.0.1", null, null));
-        when(converter.getTargetFeature()).thenReturn(feature);
-        when(converter.getRunMode(anyString())).thenReturn(feature);
+        when(converter.getFeaturesManager()).thenReturn(featuresManager);
 
         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());
         assertTrue(new File(testDirectory, "org/apache/felix/org.apache.felix.framework/6.0.1/org.apache.felix.framework-6.0.1.jar").exists());
 
-        assertFalse(converter.getTargetFeature().getBundles().isEmpty());
+        assertFalse(featuresManager.getTargetFeature().getBundles().isEmpty());
         assertEquals(1, feature.getBundles().size());
         assertEquals("org.apache.felix:org.apache.felix.framework:6.0.1", feature.getBundles().get(0).getId().toMvnId());
     }
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/handlers/ConfigEntryHandlerTest.java b/src/test/java/org/apache/sling/feature/cpconverter/handlers/ConfigEntryHandlerTest.java
index 91e65af..3dc5930 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/handlers/ConfigEntryHandlerTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/handlers/ConfigEntryHandlerTest.java
@@ -21,6 +21,9 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
@@ -35,6 +38,8 @@ import org.apache.sling.feature.ArtifactId;
 import org.apache.sling.feature.Configuration;
 import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
+import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager;
+import org.apache.sling.feature.cpconverter.features.FeaturesManager;
 import org.apache.sling.feature.io.json.FeatureJSONReader;
 import org.apache.sling.feature.io.json.FeatureJSONWriter;
 import org.junit.Test;
@@ -52,8 +57,12 @@ public class ConfigEntryHandlerTest {
         when(archive.openInputStream(entry)).thenReturn(getClass().getResourceAsStream(resourceConfiguration));
 
         Feature expected = new Feature(new ArtifactId("org.apache.sling", "org.apache.sling.cp2fm", "0.0.1", null, null));
-        ContentPackage2FeatureModelConverter converter = spy(ContentPackage2FeatureModelConverter.class);
-        when(converter.getTargetFeature()).thenReturn(expected);
+        FeaturesManager featuresManager = spy(DefaultFeaturesManager.class);
+        when(featuresManager.getTargetFeature()).thenReturn(expected);
+        doCallRealMethod().when(featuresManager).addConfiguration(anyString(), anyString(), any());
+
+        ContentPackage2FeatureModelConverter converter = mock(ContentPackage2FeatureModelConverter.class);
+        when(converter.getFeaturesManager()).thenReturn(featuresManager);
 
         new ConfigurationEntryHandler().handle(resourceConfiguration, archive, entry, converter);
 
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/handlers/ConfigurationEntryHandlerTest.java b/src/test/java/org/apache/sling/feature/cpconverter/handlers/ConfigurationEntryHandlerTest.java
index 649a07f..b2a6ee6 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/handlers/ConfigurationEntryHandlerTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/handlers/ConfigurationEntryHandlerTest.java
@@ -36,6 +36,8 @@ import org.apache.sling.feature.Configuration;
 import org.apache.sling.feature.Configurations;
 import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.cpconverter.ContentPackage2FeatureModelConverter;
+import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager;
+import org.apache.sling.feature.cpconverter.features.FeaturesManager;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -79,14 +81,16 @@ public class ConfigurationEntryHandlerTest {
         when(archive.openInputStream(entry)).thenReturn(getClass().getResourceAsStream(resourceConfiguration));
 
         Feature feature = new Feature(new ArtifactId("org.apache.sling", "org.apache.sling.cp2fm", "0.0.1", null, null));
-        ContentPackage2FeatureModelConverter converter = spy(ContentPackage2FeatureModelConverter.class);
-        when(converter.getTargetFeature()).thenReturn(feature);
-        doCallRealMethod().when(converter).addConfiguration(anyString(), anyString(), any());
-        when(converter.getRunMode(anyString())).thenReturn(feature);
+        FeaturesManager featuresManager = spy(DefaultFeaturesManager.class);
+        when(featuresManager.getTargetFeature()).thenReturn(feature);
+        doCallRealMethod().when(featuresManager).addConfiguration(anyString(), anyString(), any());
+        when(featuresManager.getRunMode(anyString())).thenReturn(feature);
+        ContentPackage2FeatureModelConverter converter = mock(ContentPackage2FeatureModelConverter.class);
+        when(converter.getFeaturesManager()).thenReturn(featuresManager);
 
         configurationEntryHandler.handle(resourceConfiguration, archive, entry, converter);
 
-        Configurations configurations = converter.getTargetFeature().getConfigurations();
+        Configurations configurations = featuresManager.getTargetFeature().getConfigurations();
 
         assertEquals(expectedConfigurationsSize, configurations.size());
 
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/handlers/RepPolicyEntryHandlerTest.java b/src/test/java/org/apache/sling/feature/cpconverter/handlers/RepPolicyEntryHandlerTest.java
index f5d6e43..80bc525 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/handlers/RepPolicyEntryHandlerTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/handlers/RepPolicyEntryHandlerTest.java
@@ -35,6 +35,8 @@ 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;
+import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager;
+import org.apache.sling.feature.cpconverter.features.FeaturesManager;
 import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageAssembler;
 import org.junit.After;
 import org.junit.Before;
@@ -158,8 +160,10 @@ public final class RepPolicyEntryHandlerTest {
         when(archive.openInputStream(entry)).thenReturn(getClass().getResourceAsStream(path));
 
         Feature feature = new Feature(new ArtifactId("org.apache.sling", "org.apache.sling.cp2fm", "0.0.1", null, null));
+        FeaturesManager featuresManager = spy(DefaultFeaturesManager.class);
+        when(featuresManager.getTargetFeature()).thenReturn(feature);
         ContentPackage2FeatureModelConverter converter = spy(ContentPackage2FeatureModelConverter.class);
-        when(converter.getTargetFeature()).thenReturn(feature);
+        when(converter.getFeaturesManager()).thenReturn(featuresManager);
 
         handler.handle(path, archive, entry, converter);
 
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/handlers/SystemUsersEntryHandlerTest.java b/src/test/java/org/apache/sling/feature/cpconverter/handlers/SystemUsersEntryHandlerTest.java
index dde6411..025feb8 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/handlers/SystemUsersEntryHandlerTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/handlers/SystemUsersEntryHandlerTest.java
@@ -33,6 +33,8 @@ 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;
+import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager;
+import org.apache.sling.feature.cpconverter.features.FeaturesManager;
 import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageAssembler;
 import org.junit.After;
 import org.junit.Before;
@@ -89,8 +91,10 @@ public class SystemUsersEntryHandlerTest {
         when(archive.openInputStream(entry)).thenReturn(getClass().getResourceAsStream(path));
 
         Feature feature = new Feature(new ArtifactId("org.apache.sling", "org.apache.sling.cp2fm", "0.0.1", null, null));
+        FeaturesManager featuresManager = spy(DefaultFeaturesManager.class);
+        when(featuresManager.getTargetFeature()).thenReturn(feature);
         ContentPackage2FeatureModelConverter converter = spy(ContentPackage2FeatureModelConverter.class);
-        when(converter.getTargetFeature()).thenReturn(feature);
+        when(converter.getFeaturesManager()).thenReturn(featuresManager);
 
         systemUsersEntryHandler.handle(path, archive, entry, converter);