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:47 UTC

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

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);
+
+}