You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by an...@apache.org on 2019/12/03 20:32:46 UTC

[sling-slingfeature-maven-plugin] branch feature/create-fm-descriptor created (now f087005)

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

andysch pushed a change to branch feature/create-fm-descriptor
in repository https://gitbox.apache.org/repos/asf/sling-slingfeature-maven-plugin.git.


      at f087005  Created new Goal to create an FM Descriptor from its POM

This branch includes the following new commits:

     new f087005  Created new Goal to create an FM Descriptor from its POM

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[sling-slingfeature-maven-plugin] 01/01: Created new Goal to create an FM Descriptor from its POM

Posted by an...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

andysch pushed a commit to branch feature/create-fm-descriptor
in repository https://gitbox.apache.org/repos/asf/sling-slingfeature-maven-plugin.git

commit f0870054dbf72d926c9f9288dc2e68b99f511a48
Author: Andreas Schaefer <sc...@iMac.local>
AuthorDate: Tue Dec 3 12:30:45 2019 -0800

    Created new Goal to create an FM Descriptor from its POM
    
    and install it in the local Maven repo for other modules to pick up with includeArtifact
---
 README.md                                          |  37 +++
 pom.xml                                            |   6 +
 .../create-fm-descriptor-simple/invoker.properties |  17 ++
 src/it/create-fm-descriptor-simple/pom.xml         |  67 +++++
 src/it/create-fm-descriptor-simple/verify.bsh      |  74 ++++++
 .../maven/mojos/CreateFeatureDescriptorMojo.java   | 144 ++++++++++
 .../mojos/manager/DefaultFeaturesManager.java      | 294 +++++++++++++++++++++
 .../maven/mojos/manager/FeaturesManager.java       |  42 +++
 .../feature/maven/mojos/manager/RunmodeMapper.java |  75 ++++++
 .../mojos/manager/SimpleVariablesInterpolator.java |  52 ++++
 .../maven/mojos/manager/VariablesInterpolator.java |  25 ++
 11 files changed, 833 insertions(+)

diff --git a/README.md b/README.md
index 1bfb2d2..bd46cbd 100644
--- a/README.md
+++ b/README.md
@@ -423,6 +423,43 @@ Beside the Feature Files this Mojo for now supports all the parameters
 of the current Feature Launcher (1.0.7-SNAPSHOT). For more info see the
 FeautreLaucherMojoTest.testFullLaunch() test method.
 
+## Create Feature Model Descriptor from POM (create-fm-descriptor)
+
+A simple Maven module creates its artifact but in order to another module
+to use that in the context of a Feature Model build the module's Feature
+Module descriptor must be created during the build.
+This can be done here:
+```
+<!-- Generate and Install the Sling OSGi Feature Model file -->
+<plugin>
+    <groupId>org.apache.sling</groupId>
+    <artifactId>slingfeature-maven-plugin</artifactId>
+    <version>${slingfeature-maven-plugin.version}</version>
+    <extensions>true</extensions>
+    <executions>
+        <execution>
+            <id>create-fm-descriptor</id>
+            <phase>package</phase>
+            <goals>
+                <goal>create-fm-descriptor</goal>
+            </goals>
+            <configuration>
+                <classifier>jcr-packageinit</classifier>
+            </configuration>
+        </execution>
+    </executions>
+</plugin>
+```
+The property **classifier** will be added only to the Feature Model
+Descriptor file name when it is installed in your local Maven repository.
+If one is provided then the user of that descriptor needs to specify it
+in the **includeArtifact**.
+The Feature Model Descriptor file name in the local Maven repository has
+this pattern:
+```
+<group id>:<artifact id>-<version>=[-<classifier>].slingosgifeature
+```
+
 ## Features Diff (features-diff)
 
 This MOJO compares different versions of the same Feature Model, producing the prototype
diff --git a/pom.xml b/pom.xml
index b3f13fe..ceefa86 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,7 @@
         <maven.version>3.5.0</maven.version>
         <maven.scm.version>1.11.1</maven.scm.version>
         <maven.site.path>${project.artifactId}-archives/${project.artifactId}-LATEST</maven.site.path>
+        <maven-artifact-transfer.version>0.11.0</maven-artifact-transfer.version>
     </properties>
 
     <scm>
@@ -310,6 +311,11 @@
             <version>3.6.0</version>
         </dependency>
         <dependency>
+            <groupId>org.apache.maven.shared</groupId>
+            <artifactId>maven-artifact-transfer</artifactId>
+            <version>${maven-artifact-transfer.version}</version>
+        </dependency>
+        <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-exec</artifactId>
             <version>1.3</version>
diff --git a/src/it/create-fm-descriptor-simple/invoker.properties b/src/it/create-fm-descriptor-simple/invoker.properties
new file mode 100644
index 0000000..209b6ef
--- /dev/null
+++ b/src/it/create-fm-descriptor-simple/invoker.properties
@@ -0,0 +1,17 @@
+# 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.
+
+invoker.goals = clean install
+invoker.debug = true
diff --git a/src/it/create-fm-descriptor-simple/pom.xml b/src/it/create-fm-descriptor-simple/pom.xml
new file mode 100644
index 0000000..5516676
--- /dev/null
+++ b/src/it/create-fm-descriptor-simple/pom.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+    agreements. See the NOTICE file distributed with this work for additional information
+    regarding copyright ownership. The ASF licenses this file to you under the Apache License,
+    Version 2.0 (the "License"); you may not use this file except in compliance with the
+    License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software distributed under the
+    License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+    either express or implied. See the License for the specific language governing permissions
+    and limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.apache.sling</groupId>
+  <artifactId>slingfeature-maven-plugin-test-create-fm-descriptor-simple</artifactId>
+  <packaging>jar</packaging>
+  <version>1.0.0-SNAPSHOT</version>
+
+  <name>Apache Sling Features Maven plugin test - create fm descriptor simple</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.codehaus.janino</groupId>
+      <artifactId>janino</artifactId>
+      <version>2.7.5</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.apache.ant</groupId>
+          <artifactId>ant-nodeps</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.sling</groupId>
+      <artifactId>org.apache.sling.commons.testing</artifactId>
+      <version>2.1.2</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>@project.groupId@</groupId>
+        <artifactId>@project.artifactId@</artifactId>
+        <version>@project.version@</version>
+        <extensions>true</extensions>
+        <executions>
+          <execution>
+            <id>analyze</id>
+            <phase>package</phase>
+            <goals>
+              <goal>create-fm-descriptor</goal>
+            </goals>
+            <configuration>
+              <classifier>test</classifier>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+</project>
diff --git a/src/it/create-fm-descriptor-simple/verify.bsh b/src/it/create-fm-descriptor-simple/verify.bsh
new file mode 100644
index 0000000..c5d8872
--- /dev/null
+++ b/src/it/create-fm-descriptor-simple/verify.bsh
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+import java.io.*;
+import java.util.*;
+import org.codehaus.plexus.util.*;
+
+    boolean check() {
+        // Check folder and file existence
+
+        String group = "org.apache.sling";
+        String groupPath = group.replaceAll("\\.", "/");
+        String artifact = "slingfeature-maven-plugin-test-create-fm-descriptor-simple";
+        String version = "1.0.0-SNAPSHOT";
+        File localMavenRepositoryInstallationFolder = new File(
+            localRepositoryPath, groupPath + "/" + artifact + "/" + version
+        );
+        if(!localMavenRepositoryInstallationFolder.exists()) {
+            System.out.println("Installation Folder does not exist: " + localMavenRepositoryInstallationFolder);
+            return false;
+        }
+
+        String classifier = "test";
+        String extension = "slingosgifeature";
+        File fmDescriptorFile = new File(
+            localMavenRepositoryInstallationFolder, artifact + "-" + version + "-" + classifier + "." + extension
+        );
+        if(!fmDescriptorFile.exists()) {
+            System.out.println("FM Descriptor file does not exist: " + fmDescriptorFile);
+            return false;
+        }
+
+        // Check FM Descriptor Content
+        String fmContent = FileUtils.fileRead(fmDescriptorFile);
+        System.out.println("FM Descriptor File Content: " + fmContent);
+        String dependentGroup = "org.codehaus.janino";
+        String dependentArtifact = "janino";
+        String dependentVersion = "2.7.5";
+        String[] values = {
+            "\"id\":\"" + group + ":" + artifact + ":slingosgifeature:" + version + "\"",
+            "\"bundles\":[",
+            "\"id\":\"" + group + ":" + artifact + ":" + version + "\"",
+            "\"id\":\"" + dependentGroup + ":" + dependentArtifact + ":" + dependentVersion + "\"",
+        };
+        for (String value : values) {
+            if (fmContent.indexOf(value) < 0) {
+                System.out.println("Did not find line: " + value + " -> FAILED!");
+                return false;
+            }
+        }
+
+        return true;
+    }
+    try {
+        return check();
+    }
+    catch(Throwable t) {
+        t.printStackTrace();
+        return false;
+    }
+    return true;
diff --git a/src/main/java/org/apache/sling/feature/maven/mojos/CreateFeatureDescriptorMojo.java b/src/main/java/org/apache/sling/feature/maven/mojos/CreateFeatureDescriptorMojo.java
new file mode 100644
index 0000000..21537af
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/maven/mojos/CreateFeatureDescriptorMojo.java
@@ -0,0 +1,144 @@
+/*
+ * 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.maven.mojos;
+
+import org.apache.maven.artifact.Artifact;
+import org.apache.maven.artifact.DefaultArtifact;
+import org.apache.maven.artifact.handler.DefaultArtifactHandler;
+import org.apache.maven.model.Dependency;
+import org.apache.maven.shared.transfer.artifact.install.ArtifactInstaller;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Component;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.ProjectBuildingRequest;
+import org.apache.maven.shared.transfer.artifact.install.ArtifactInstallerException;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.maven.mojos.manager.DefaultFeaturesManager;
+import org.apache.sling.feature.maven.mojos.manager.FeaturesManager;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Create a Feature Model Descriptor (slingosigfeature) based on the
+ * project's POM
+ */
+@Mojo(
+    name = "create-fm-descriptor",
+    requiresProject = true,
+    threadSafe = true
+)
+public class CreateFeatureDescriptorMojo extends AbstractIncludingFeatureMojo {
+
+    public static final String CFG_VERBOSE = "verbose";
+    public static final String CFG_CLASSIFIER = "classifier";
+
+    public static final String CFG_FM_OUTPUT_DIRECTORY = "featureModelsOutputDirectory";
+    public static final String DEFAULT_FM_OUTPUT_DIRECTORY = "${project.build.directory}/fm";
+
+    /**
+     * Target directory for the Feature Model file
+     */
+    @Parameter(property = CFG_FM_OUTPUT_DIRECTORY, defaultValue = DEFAULT_FM_OUTPUT_DIRECTORY)
+    private File fmOutput;
+
+    /**
+     * Optional Classifier for this Feature Model Descriptor file name
+     */
+    @Parameter(property = CFG_CLASSIFIER, required = false, defaultValue = "")
+    private String classifier;
+
+    /**
+     * Framework Properties for the Launcher
+     */
+    @Parameter(property = CFG_VERBOSE, required = false, defaultValue = "false")
+    private boolean verbose;
+
+    @Component
+    protected ArtifactInstaller installer;
+
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        checkPreconditions();
+        // Create a Feature Model Descriptor with its bundle in it
+        FeaturesManager featuresManager = new DefaultFeaturesManager(
+            true, 20, fmOutput, null, null, null
+        );
+        featuresManager.init(project.getGroupId(), project.getArtifactId(), project.getVersion());
+        ArtifactId bundle = new ArtifactId(project.getGroupId(), project.getArtifactId(), project.getVersion(), "", "bundle");
+        featuresManager.addArtifact("", bundle);
+        List<Dependency> dependencies = project.getDependencies();
+        for(Dependency dependency: dependencies) {
+            if("compile".equals(dependency.getScope())) {
+                featuresManager.addArtifact("", new ArtifactId(
+                    dependency.getGroupId(), dependency.getArtifactId(), dependency.getVersion(), dependency.getClassifier(), dependency.getType()
+                ));
+            }
+        }
+        getLog().info("Project Features: " + featuresManager);
+        try {
+            featuresManager.serialize();
+            // Now install the file as .slingosigfeature into the local Maven Repo
+            installFMDescriptor(bundle);
+        } catch(Exception e) {
+            getLog().error("Failure to serialize Feature Manager", e);
+        }
+    }
+    private void installFMDescriptor(ArtifactId artifact) {
+        Collection<Artifact> artifacts = Collections.synchronizedCollection(new ArrayList<>());
+        // Source FM Descriptor File Path
+        String fmDescriptorFilePath = artifact.getArtifactId() + ".json";
+        File fmDescriptorFile = new File(fmOutput, fmDescriptorFilePath);
+        if(fmDescriptorFile.exists() && fmDescriptorFile.canRead()) {
+            // Need to create a new Artifact Handler for the different extension and an Artifact to not
+            // change the module artifact
+            DefaultArtifactHandler fmArtifactHandler = new DefaultArtifactHandler("slingosgifeature");
+            //AS TODO: For now the classifier is not set (check if we need to add the artifact id)
+            DefaultArtifact fmArtifact = new DefaultArtifact(
+                artifact.getGroupId(), artifact.getArtifactId(), artifact.getVersion(),
+                null, "slingosgifeature", classifier, fmArtifactHandler
+            );
+            fmArtifact.setFile(fmDescriptorFile);
+            artifacts.add(fmArtifact);
+            try {
+                installArtifact(mavenSession.getProjectBuildingRequest(), artifacts);
+            } catch (MojoFailureException | MojoExecutionException e) {
+                getLog().error("Failed to install FM Descriptor", e);
+            }
+        } else {
+            getLog().error("Could not find FM Descriptor File: " + fmDescriptorFile);
+        }
+    }
+
+    private void installArtifact(ProjectBuildingRequest pbr, Collection<Artifact> artifacts )
+        throws MojoFailureException, MojoExecutionException
+    {
+        try
+        {
+            installer.install(pbr, artifacts);
+        }
+        catch ( ArtifactInstallerException e )
+        {
+            throw new MojoExecutionException( "ArtifactInstallerException", e );
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/maven/mojos/manager/DefaultFeaturesManager.java b/src/main/java/org/apache/sling/feature/maven/mojos/manager/DefaultFeaturesManager.java
new file mode 100644
index 0000000..f283aaa
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/maven/mojos/manager/DefaultFeaturesManager.java
@@ -0,0 +1,294 @@
+/*
+ * 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.maven.mojos.manager;
+
+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.io.json.FeatureJSONWriter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import static java.util.Objects.requireNonNull;
+
+public class DefaultFeaturesManager implements FeaturesManager {
+
+    public static final String ZIP_TYPE = "zip";
+
+    private static final String JAVA_IO_TMPDIR_PROPERTY = "java.io.tmpdir";
+
+    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 String prefix;
+
+    private final Map<String, String> properties;
+
+    private final List<String> targetAPIRegions = new ArrayList<>();
+
+    private Feature targetFeature = null;
+
+    public DefaultFeaturesManager() {
+        this(true, 20, new File(System.getProperty(JAVA_IO_TMPDIR_PROPERTY)), null, null, null);
+    }
+
+    public DefaultFeaturesManager(boolean mergeConfigurations,
+                                  int bundlesStartOrder,
+                                  File featureModelsOutputDirectory,
+                                  String artifactIdOverride,
+                                  String prefix,
+                                  Map<String, String> properties) {
+        this.mergeConfigurations = mergeConfigurations;
+        this.bundlesStartOrder = bundlesStartOrder;
+        this.featureModelsOutputDirectory = featureModelsOutputDirectory;
+        this.artifactIdOverride = artifactIdOverride;
+        this.prefix = prefix;
+        this.properties = properties;
+    }
+
+    public void init(String groupId, String artifactId, String version) {
+        targetFeature = new Feature(new ArtifactId(groupId, artifactId, version, null, SLING_OSGI_FEATURE_TILE_TYPE));
+
+        initAPIRegions(targetFeature);
+
+        runModes.clear();
+    }
+
+    private void initAPIRegions(Feature feature) {
+        if (targetAPIRegions.size() > 0) {
+            Extension apiRegions = new Extension(ExtensionType.JSON, "api-regions", false);
+            StringBuilder jsonBuilder = new StringBuilder("[");
+            for (String apiRegion : targetAPIRegions) {
+                if (jsonBuilder.length() > 1) {
+                    jsonBuilder.append(',');
+                }
+                jsonBuilder.append("{\"name\":\"");
+                jsonBuilder.append(apiRegion);
+                jsonBuilder.append("\",\"exports\":[]}");
+            }
+            jsonBuilder.append("]");
+            apiRegions.setJSON(jsonBuilder.toString());
+            feature.getExtensions().add(apiRegions);
+        }
+    }
+
+    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 -> {
+            Feature f = new Feature(newId);
+            initAPIRegions(f);
+            return f;
+        });
+    }
+
+    public void addArtifact(String runMode, ArtifactId id) {
+        addArtifact(runMode, id, null);
+    }
+
+    public void addArtifact(String runMode, ArtifactId id, Integer startOrder) {
+        requireNonNull(id, "Artifact can not be attached to a feature without specifying a valid ArtifactId.");
+
+        Artifact artifact = new Artifact(id);
+
+        Feature targetFeature = getRunMode(runMode);
+        Artifacts artifacts;
+
+        if (ZIP_TYPE.equals(id.getType()) ) {
+            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 {
+            int startOrderForBundle = startOrder != null ? startOrder.intValue() : bundlesStartOrder;
+            artifact.setStartOrder(startOrderForBundle);
+            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);
+
+        serialize(targetFeature, null, runmodeMapper);
+
+        if (!runModes.isEmpty()) {
+            for (Entry<String, Feature> runmodeEntry : runModes.entrySet()) {
+                String runmode = runmodeEntry.getKey();
+                serialize(runmodeEntry.getValue(), runmode, runmodeMapper);
+            }
+        }
+
+        runmodeMapper.save();
+    }
+
+    private void serialize(Feature feature, String runMode, RunmodeMapper runmodeMapper) throws Exception {
+        StringBuilder fileNameBuilder = new StringBuilder()
+            .append((prefix != null) ? prefix : "")
+            .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);
+        if (!targetFile.getParentFile().exists()) {
+            targetFile.getParentFile().mkdirs();
+        }
+
+        if (artifactIdOverride != null && !artifactIdOverride.isEmpty()) {
+            String interpolatedIdOverride = interpolator.interpolate(artifactIdOverride, properties);
+            ArtifactId idOverrride = appendRunmode(ArtifactId.parse(interpolatedIdOverride), runMode);
+            feature = feature.copy(idOverrride);
+        }
+
+        logger.info("Writing resulting Feature Model '{}' to file '{}'...", feature.getId(), targetFile);
+
+        try (FileWriter targetWriter = new FileWriter(targetFile)) {
+            FeatureJSONWriter.write(targetWriter, feature);
+
+            logger.info("'{}' Feature File successfully written!", targetFile);
+
+            runmodeMapper.addOrUpdate(runMode, fileName);
+        }
+    }
+
+    public synchronized DefaultFeaturesManager setAPIRegions(List<String> regions) {
+        targetAPIRegions.clear();
+        targetAPIRegions.addAll(regions);
+        return this;
+    }
+
+    @Override
+    public void addOrAppendRepoInitExtension(String text) {
+        
+        Extension repoInitExtension = getTargetFeature().getExtensions().getByName(Extension.EXTENSION_NAME_REPOINIT);
+        
+        if (repoInitExtension == null) {
+            repoInitExtension = new Extension(ExtensionType.TEXT, Extension.EXTENSION_NAME_REPOINIT, true);
+            getTargetFeature().getExtensions().add(repoInitExtension);
+            repoInitExtension.setText(text);
+        } else {
+            repoInitExtension.setText(repoInitExtension.getText() + "\n " + text);
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/feature/maven/mojos/manager/FeaturesManager.java b/src/main/java/org/apache/sling/feature/maven/mojos/manager/FeaturesManager.java
new file mode 100644
index 0000000..ac7d1a2
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/maven/mojos/manager/FeaturesManager.java
@@ -0,0 +1,42 @@
+/*
+ * 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.maven.mojos.manager;
+
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+
+import java.util.Dictionary;
+
+public interface FeaturesManager {
+
+    void init(String groupId, String artifactId, String version);
+
+    Feature getTargetFeature();
+
+    Feature getRunMode(String runMode);
+
+    void addArtifact(String runMode, ArtifactId id);
+
+    void addArtifact(String runMode, ArtifactId id, Integer startOrder);
+
+    void addConfiguration(String runMode, String pid, Dictionary<String, Object> configurationProperties);
+
+    void serialize() throws Exception;
+
+    void addOrAppendRepoInitExtension(String text);
+
+}
diff --git a/src/main/java/org/apache/sling/feature/maven/mojos/manager/RunmodeMapper.java b/src/main/java/org/apache/sling/feature/maven/mojos/manager/RunmodeMapper.java
new file mode 100644
index 0000000..eb161e3
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/maven/mojos/manager/RunmodeMapper.java
@@ -0,0 +1,75 @@
+/*
+ * 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.maven.mojos.manager;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+final class RunmodeMapper {
+
+    private static final String FILENAME = "runmode.mapping";
+
+    public static RunmodeMapper open(File featureModelsOutputDirectory) throws IOException {
+        Properties properties = new Properties();
+
+        File runmodeMappingFile = new File(featureModelsOutputDirectory, FILENAME);
+        if (runmodeMappingFile.exists()) {
+            try (FileInputStream input = new FileInputStream(runmodeMappingFile)) {
+                properties.load(input);
+            }
+        }
+
+        return new RunmodeMapper(runmodeMappingFile, properties);
+    }
+
+    private static final String DEFAULT = "(default)";
+
+    private final File runmodeMappingFile;
+
+    private final Properties properties;
+
+    private RunmodeMapper(File runmodeMappingFile, Properties properties) {
+        this.runmodeMappingFile = runmodeMappingFile;
+        this.properties = properties;
+    }
+
+    public void addOrUpdate(String runMode, String jsonFileName) {
+        if (runMode == null) {
+            runMode = DEFAULT;
+        }
+
+        String value = properties.getProperty(runMode);
+
+        if (value != null && !value.contains(jsonFileName)) {
+            value += ',' + jsonFileName;
+        } else {
+            value = jsonFileName;
+        }
+
+        properties.setProperty(runMode, value);
+    }
+
+    public void save() throws IOException {
+        try (FileOutputStream output = new FileOutputStream(runmodeMappingFile)) {
+            properties.store(output, "File edited by the Apache Sling Content Package to Sling Feature converter");
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/feature/maven/mojos/manager/SimpleVariablesInterpolator.java b/src/main/java/org/apache/sling/feature/maven/mojos/manager/SimpleVariablesInterpolator.java
new file mode 100644
index 0000000..a9acc56
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/maven/mojos/manager/SimpleVariablesInterpolator.java
@@ -0,0 +1,52 @@
+/*
+ * 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.maven.mojos.manager;
+
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static java.util.Objects.requireNonNull;
+
+public final class SimpleVariablesInterpolator implements VariablesInterpolator {
+
+    private final Pattern replacementPattern = Pattern.compile("\\$\\{\\{(.+?)\\}\\}");
+
+    public String interpolate(String format, Map<String, String> properties) {
+        requireNonNull(format, "Input string format must be not null");
+
+        if (properties == null || properties.isEmpty()) {
+            return format;
+        }
+
+        Matcher matcher = replacementPattern.matcher(format);
+        StringBuffer result = new StringBuffer();
+        while (matcher.find()) {
+            String variable = matcher.group(1);
+            String resolved = properties.get(variable);
+
+            if (resolved == null) {
+                resolved = String.format("\\$\\{\\{%s\\}\\}", variable);
+            }
+
+            matcher.appendReplacement(result, resolved);
+        }
+        matcher.appendTail(result);
+        return result.toString();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/feature/maven/mojos/manager/VariablesInterpolator.java b/src/main/java/org/apache/sling/feature/maven/mojos/manager/VariablesInterpolator.java
new file mode 100644
index 0000000..0759967
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/maven/mojos/manager/VariablesInterpolator.java
@@ -0,0 +1,25 @@
+/*
+ * 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.maven.mojos.manager;
+
+import java.util.Map;
+
+public interface VariablesInterpolator {
+
+    String interpolate(String format, Map<String, String> properties);
+
+}