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

[sling-slingfeature-maven-plugin] branch master updated: SLING-8640 : Add a Feature Launcher Mojo to Sling Feature Maven Plugin

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

cziegeler pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sling-slingfeature-maven-plugin.git


The following commit(s) were added to refs/heads/master by this push:
     new 3e04fec  SLING-8640 : Add a Feature Launcher Mojo to Sling Feature Maven Plugin
3e04fec is described below

commit 3e04fec736617fec1ddac49c2e0a1c1f3a0869e7
Author: Andreas Schaefer <sc...@me.com>
AuthorDate: Tue Aug 20 13:17:11 2019 -0700

    SLING-8640 : Add a Feature Launcher Mojo to Sling Feature Maven Plugin
    
    * Created a Feature Launcher Mojo (launch-features)
    
    * Added an unit test for the Feature Launcher Mojo
    
    * Added Copyright to new file
    
    * Added an assert to the test to fix a code smell
    
    * Some more code converage for this PR
    
    * SLING-8640 - Moved the Launcher to use Feature Selection instead of a simple file
    
    Also removed duplicate code
    
    * SLING-8640 - Added documentation of the Feature Launcher Mojo to the Readme file
    
    fixed some of the code coverage tests and added two more unit tests.
    
    * Added the license header
---
 README.md                                          |  64 +++++
 pom.xml                                            |   6 +
 .../feature/maven/mojos/AbstractFeatureMojo.java   |  78 ++++---
 .../feature/maven/mojos/AggregateFeaturesMojo.java |  53 +----
 .../feature/maven/mojos/FeatureLauncherMojo.java   | 199 ++++++++++++++++
 .../META-INF/m2e/lifecycle-mapping-metadata.xml    |   1 +
 .../apache/sling/feature/launcher/impl/Main.java   |  37 +++
 .../maven/mojos/FeatureLauncherMojoTest.java       | 257 +++++++++++++++++++++
 .../test-aggregated-feature-example-runtime.json   |  25 ++
 9 files changed, 635 insertions(+), 85 deletions(-)

diff --git a/README.md b/README.md
index c4ee341..bc584ae 100644
--- a/README.md
+++ b/README.md
@@ -267,3 +267,67 @@ With the repository goal, a directory with all artifacts from the selected featu
    </configuration>
  </execution>
 ```
+
+### Feature Launcher (launch-features)
+
+**Attention**: This Mojo is BETA meaning under development and new released
+may change the way the Mojo is used.
+
+This Mojo allows the user to launch a Feature(s) from a POM through a
+profile. This can look like this:
+```
+<profile>
+    <id>launch</id>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.sling</groupId>
+                <artifactId>slingfeature-maven-plugin</artifactId>
+                <version>1.0.7-SNAPSHOT</version>
+                <extensions>true</extensions>
+                <dependencies>
+                    <!-- To Support the Deployment of Content Package the Extension Content
+                         must be added BEFORE the Feature Launcher -->
+                    <dependency>
+                        <groupId>org.apache.sling</groupId>
+                        <artifactId>org.apache.sling.feature.extension.content</artifactId>
+                        <version>1.0.5-SNAPSHOT</version>
+                    </dependency>
+                    <dependency>
+                        <groupId>org.apache.sling</groupId>
+                        <artifactId>org.apache.sling.feature.launcher</artifactId>
+                        <version>1.0.7-SNAPSHOT</version>
+                    </dependency>
+                </dependencies>
+                <executions>
+                    <execution>
+                        <id>launch-it</id>
+                        <phase>install</phase>
+                        <goals>
+                            <goal>launch-features</goal>
+                        </goals>
+                        <configuration>
+                            <selection>
+                                <includeClassifier>example-runtime</includeClassifier>
+                            </selection>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</profile>
+```
+
+Do avoid having to release this plugin every time a new Feature Launcher
+is released the two are decoupled and the Feature Launcher is loaded at
+runtime instead. For that the feature launcher must be added to the plugin
+as dependency. If missing this Mojo with fail as it cannot find the launcher.
+
+**Attention**: to deploy converted Content Packages the **Feature Content
+Extension must added here as well and it must be place **AHEAD** of the
+Feature Launcher.
+
+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.
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index e4b273f..622f3da 100644
--- a/pom.xml
+++ b/pom.xml
@@ -318,6 +318,12 @@
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>1.6.2</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <reporting>
diff --git a/src/main/java/org/apache/sling/feature/maven/mojos/AbstractFeatureMojo.java b/src/main/java/org/apache/sling/feature/maven/mojos/AbstractFeatureMojo.java
index 7e7050d..62b5e26 100644
--- a/src/main/java/org/apache/sling/feature/maven/mojos/AbstractFeatureMojo.java
+++ b/src/main/java/org/apache/sling/feature/maven/mojos/AbstractFeatureMojo.java
@@ -273,49 +273,53 @@ public abstract class AbstractFeatureMojo extends AbstractMojo {
     }
 
     private BuilderContext getBuilderContext() {
-        final BuilderContext builderContext = new BuilderContext(new FeatureProvider() {
-            @Override
-            public Feature provide(ArtifactId id) {
-                // Check for the feature in the local context
-                for (final Feature feat : ProjectHelper.getAssembledFeatures(project).values()) {
-                    if (feat.getId().equals(id)) {
-                        return feat;
-                    }
-                }
+        final BuilderContext builderContext = new BuilderContext(new BaseFeatureProvider())
+            .setArtifactProvider(new BaseArtifactProvider());
+
+        return builderContext;
+    }
 
-                if (ProjectHelper.isLocalProjectArtifact(project, id)) {
-                    throw new RuntimeException("Unable to resolve local artifact " + id.toMvnId());
+    protected class BaseFeatureProvider implements FeatureProvider {
+        @Override
+        public Feature provide(ArtifactId id) {
+            // Check for the feature in the local context
+            for (final Feature feat : ProjectHelper.getAssembledFeatures(project).values()) {
+                if (feat.getId().equals(id)) {
+                    return feat;
                 }
+            }
 
-                // Finally, look the feature up via Maven's dependency mechanism
-                return ProjectHelper.getOrResolveFeature(project, mavenSession, artifactHandlerManager,
-                        artifactResolver, id);
+            if (ProjectHelper.isLocalProjectArtifact(project, id)) {
+                throw new RuntimeException("Unable to resolve local artifact " + id.toMvnId());
             }
-        }).setArtifactProvider(new ArtifactProvider() {
-
-            @Override
-            public URL provide(final ArtifactId id) {
-                if (ProjectHelper.isLocalProjectArtifact(project, id)) {
-                    for (final Map.Entry<String, Feature> entry : ProjectHelper.getAssembledFeatures(project)
-                            .entrySet()) {
-                        if (entry.getValue().getId().equals(id)) {
-                            // TODO - we might need to create a file to return it here
-                            throw new RuntimeException(
-                                    "Unable to get file for project feature " + entry.getValue().getId().toMvnId());
-                        }
+
+            // Finally, look the feature up via Maven's dependency mechanism
+            return ProjectHelper.getOrResolveFeature(project, mavenSession, artifactHandlerManager,
+                artifactResolver, id);
+        }
+    }
+
+    protected class BaseArtifactProvider implements ArtifactProvider {
+        @Override
+        public URL provide(final ArtifactId id) {
+            if (ProjectHelper.isLocalProjectArtifact(project, id)) {
+                for (final Map.Entry<String, Feature> entry : ProjectHelper.getAssembledFeatures(project)
+                    .entrySet()) {
+                    if (entry.getValue().getId().equals(id)) {
+                        // TODO - we might need to create a file to return it here
+                        throw new RuntimeException(
+                            "Unable to get file for project feature " + entry.getValue().getId().toMvnId());
                     }
                 }
-                try {
-                    return ProjectHelper
-                            .getOrResolveArtifact(project, mavenSession, artifactHandlerManager, artifactResolver, id)
-                            .getFile().toURI().toURL();
-                } catch (Exception e) {
-                    getLog().error(e);
-                    return null;
-                }
             }
-        });
-
-        return builderContext;
+            try {
+                return ProjectHelper
+                    .getOrResolveArtifact(project, mavenSession, artifactHandlerManager, artifactResolver, id)
+                    .getFile().toURI().toURL();
+            } catch (Exception e) {
+                getLog().error(e);
+                return null;
+            }
+        }
     }
 }
diff --git a/src/main/java/org/apache/sling/feature/maven/mojos/AggregateFeaturesMojo.java b/src/main/java/org/apache/sling/feature/maven/mojos/AggregateFeaturesMojo.java
index cfe8ce3..bacc699 100644
--- a/src/main/java/org/apache/sling/feature/maven/mojos/AggregateFeaturesMojo.java
+++ b/src/main/java/org/apache/sling/feature/maven/mojos/AggregateFeaturesMojo.java
@@ -17,7 +17,6 @@
 package org.apache.sling.feature.maven.mojos;
 
 import java.io.File;
-import java.net.URL;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -29,17 +28,14 @@ import java.util.Spliterators;
 import java.util.stream.StreamSupport;
 
 import org.apache.maven.plugin.MojoExecutionException;
-import org.apache.maven.plugin.MojoFailureException;
 import org.apache.maven.plugins.annotations.LifecyclePhase;
 import org.apache.maven.plugins.annotations.Mojo;
 import org.apache.maven.plugins.annotations.Parameter;
 import org.apache.maven.plugins.annotations.ResolutionScope;
 import org.apache.sling.feature.ArtifactId;
 import org.apache.sling.feature.Feature;
-import org.apache.sling.feature.builder.ArtifactProvider;
 import org.apache.sling.feature.builder.BuilderContext;
 import org.apache.sling.feature.builder.FeatureBuilder;
-import org.apache.sling.feature.builder.FeatureProvider;
 import org.apache.sling.feature.builder.MergeHandler;
 import org.apache.sling.feature.builder.PostProcessHandler;
 import org.apache.sling.feature.maven.FeatureConstants;
@@ -67,7 +63,7 @@ public class AggregateFeaturesMojo extends AbstractIncludingFeatureMojo {
     Map<String, Properties> handlerConfiguration = new HashMap<>();
 
     @Override
-    public void execute() throws MojoExecutionException, MojoFailureException {
+    public void execute() throws MojoExecutionException {
         checkPreconditions();
         for (final Aggregate aggregate : aggregates) {
             // check classifier
@@ -86,7 +82,7 @@ public class AggregateFeaturesMojo extends AbstractIncludingFeatureMojo {
             if (aggregate.frameworkPropertiesOverrides != null)
                 frameworkPropertiesOverwrites.putAll(aggregate.frameworkPropertiesOverrides);
 
-            final BuilderContext builderContext = new BuilderContext(new FeatureProvider() {
+            final BuilderContext builderContext = new BuilderContext(new BaseFeatureProvider() {
                 @Override
                 public Feature provide(ArtifactId id) {
                     // check in selection
@@ -95,49 +91,10 @@ public class AggregateFeaturesMojo extends AbstractIncludingFeatureMojo {
                             return feat;
                         }
                     }
-
-                    // Check for the feature in the local context
-                    for (final Feature feat : ProjectHelper.getAssembledFeatures(project).values()) {
-                        if (feat.getId().equals(id)) {
-                            return feat;
-                        }
-                    }
-
-                    if (ProjectHelper.isLocalProjectArtifact(project, id)) {
-                        throw new RuntimeException("Unable to resolve local artifact " + id.toMvnId());
-                    }
-
-                    // Finally, look the feature up via Maven's dependency mechanism
-                    return ProjectHelper.getOrResolveFeature(project, mavenSession, artifactHandlerManager,
-                            artifactResolver, id);
-                }
-            }).setArtifactProvider(new ArtifactProvider() {
-
-                @Override
-                public URL provide(final ArtifactId id) {
-                    if (ProjectHelper.isLocalProjectArtifact(project, id)) {
-                        for (final Map.Entry<String, Feature> entry : ProjectHelper.getAssembledFeatures(project)
-                                .entrySet()) {
-                            if (entry.getValue().getId().equals(id)) {
-                                // TODO - we might need to create a file to return it here
-                                throw new RuntimeException(
-                                        "Unable to get file for project feature " + entry.getValue().getId().toMvnId());
-                            }
-                        }
-                    }
-                    try
-                    {
-                        return ProjectHelper
-                                .getOrResolveArtifact(project, mavenSession, artifactHandlerManager, artifactResolver, id)
-                                .getFile().toURI().toURL();
-                    }
-                    catch (Exception e)
-                    {
-                        getLog().error(e);
-                        return null;
-                    }
+                    return super.provide(id);
                 }
-            }).addVariablesOverrides(variablesOverwrites)
+            }).setArtifactProvider(new BaseArtifactProvider())
+                .addVariablesOverrides(variablesOverwrites)
                 .addFrameworkPropertiesOverrides(frameworkPropertiesOverwrites)
                 .addMergeExtensions(StreamSupport.stream(Spliterators.spliteratorUnknownSize(
                     ServiceLoader.load(MergeHandler.class).iterator(), Spliterator.ORDERED),
diff --git a/src/main/java/org/apache/sling/feature/maven/mojos/FeatureLauncherMojo.java b/src/main/java/org/apache/sling/feature/maven/mojos/FeatureLauncherMojo.java
new file mode 100644
index 0000000..6b6eb77
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/maven/mojos/FeatureLauncherMojo.java
@@ -0,0 +1,199 @@
+/*
+ * 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 com.google.common.io.Files;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.io.json.FeatureJSONWriter;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Launches the given Feature File
+ */
+@Mojo(
+    name = "launch-features",
+    requiresProject = true,
+    threadSafe = true
+)
+public class FeatureLauncherMojo extends AbstractIncludingFeatureMojo {
+
+    public static final String CFG_ARTIFACT_CLASH_OVERRIDES = "artifactClashOverrides";
+    public static final String CFG_REPOSITORY_URL = "frameworkRepositoryUrl";
+    public static final String CFG_FRAMEWORK_PROPERTIES = "frameworkProperties";
+    public static final String CFG_VARIABLE_VALUES = "variableValues";
+    public static final String CFG_VERBOSE = "verbose";
+    public static final String CFG_CACHE_DIRECTORY = "cacheDirectory";
+    public static final String CFG_HOME_DIRECTORY = "homeDirectory";
+    public static final String CFG_EXTENSION_CONFIGURATIONS = "extensionConfigurations";
+    public static final String CFG_FRAMEWORK_VERSION = "frameworkVersion";
+    public static final String CFG_FRAMEWORK_ARTIFACTS = "frameworkArtifacts";
+
+    @Parameter
+    private FeatureSelectionConfig selection;
+
+    /**
+     * The Artifact Id Overrides (see Feature Launcher for more info)
+     */
+    @Parameter(property = CFG_ARTIFACT_CLASH_OVERRIDES, required = false)
+    private String[] artifactClashOverrides;
+
+    /**
+     * The Url for the Repository (see Feature Launcher for more info)
+     */
+    @Parameter(property = CFG_REPOSITORY_URL, required = false)
+    private String repositoryUrl;
+
+    /**
+     * Framework Properties for the Launcher
+     */
+    @Parameter(property = CFG_FRAMEWORK_PROPERTIES, required = false)
+    private String[] frameworkProperties;
+
+    /**
+     * Variable Values for the Launcher
+     */
+    @Parameter(property = CFG_VARIABLE_VALUES, required = false)
+    private String[] variableValues;
+
+    /**
+     * Framework Properties for the Launcher
+     */
+    @Parameter(property = CFG_VERBOSE, required = false, defaultValue = "false")
+    private boolean verbose;
+
+    /**
+     * The path of the folder where the cache is located
+     */
+    @Parameter(property = CFG_CACHE_DIRECTORY, required = false)
+    private File cacheDirectory;
+
+    /**
+     * The path of the folder where the launcher home is located
+     */
+    @Parameter(property = CFG_HOME_DIRECTORY, required = false)
+    private File homeDirectory;
+
+    /**
+     * Extension Configurations for the Launcher
+     */
+    @Parameter(property = CFG_EXTENSION_CONFIGURATIONS, required = false)
+    private String[] extensionConfigurations;
+
+    /**
+     * The Framework Version (see Feature Launcher for more info)
+     */
+    @Parameter(property = CFG_FRAMEWORK_VERSION, required = false)
+    private String frameworkVersion;
+
+    /**
+     * Framework Artifacts for the Launcher
+     */
+    @Parameter(property = CFG_FRAMEWORK_ARTIFACTS, required = false)
+    private String[] frameworkArtifacts;
+
+    @Override
+    public void execute() throws MojoExecutionException, MojoFailureException {
+        checkPreconditions();
+        List<String> arguments = new ArrayList<>();
+        getLog().info("Feature Selection: " + selection);
+        final Collection<Feature> features = getSelectedFeatures(selection).values();
+        getLog().info("Features from Selection: " + features);
+        for(Feature feature: features) {
+            // Loop over all features found, create a temporary file, write the features there and add them to the launcher's file list
+            File folder = Files.createTempDir();
+            ArtifactId id = feature.getId();
+            File featureFile = new File(folder, id.toMvnId().replaceAll(":", "-") + ".json");
+            // TODO: Do we need to support Prototypes etc?
+            try ( final Writer writer = new FileWriter(featureFile)) {
+                FeatureJSONWriter.write(writer, feature);
+            } catch (final IOException e) {
+                throw new MojoExecutionException("Unable to write feature file  :" + id.toMvnId(), e);
+            }
+            getLog().info("Feature File Location: " + featureFile);
+            handleFile(arguments, featureFile, "-f");
+        }
+        handleStringList(arguments, artifactClashOverrides, "-C");
+        handleString(arguments, repositoryUrl, "-u");
+        handleStringList(arguments, frameworkProperties, "-D");
+        handleStringList(arguments, variableValues, "-V");
+        if(verbose) {
+            arguments.add("-v");
+        }
+        handleFile(arguments, cacheDirectory, "-c");
+        handleFile(arguments, homeDirectory, "-p");
+        handleStringList(arguments, extensionConfigurations, "-ec");
+        handleString(arguments, frameworkVersion, "-fw");
+        handleStringList(arguments, frameworkArtifacts, "-fa");
+
+        String[] args = arguments.toArray(new String[] {});
+        getLog().info("Launcher Arguments: '" + arguments + "'");
+        launch(args);
+    }
+
+    void launch(String[] arguments) throws MojoExecutionException {
+        try {
+            Class clazz = Thread.currentThread().getContextClassLoader().loadClass(
+                "org.apache.sling.feature.launcher.impl.Main"
+            );
+            Method main = clazz.getMethod("main", String[].class);
+            main.invoke(null, (Object) arguments);
+        } catch (ClassNotFoundException | NoSuchMethodException e) {
+            throw new MojoExecutionException("Failed to load Feature Launcher or Method not available, make sure the Launcher Dependency is added to the Plugin", e);
+        } catch (InvocationTargetException e) {
+            throw new MojoExecutionException("Invocation of Launcher's Main.main() failed", e.getCause());
+        } catch (IllegalAccessException | IllegalArgumentException e) {
+            throw new MojoExecutionException("Access denied or wrong Arguments", e);
+        }
+    }
+
+    private void handleStringList(List<String> arguments, String[] list, String parameter) {
+        if(list != null) {
+            for(String item: list) {
+                arguments.add(parameter);
+                arguments.add(item);
+            }
+        }
+    }
+
+    private void handleString(List<String> arguments, String item, String parameter) {
+        if(item != null && !item.isEmpty()) {
+            arguments.add(parameter);
+            arguments.add(item);
+        }
+    }
+
+    private void handleFile(List<String> arguments, File file, String parameter) {
+        if(file != null) {
+            arguments.add(parameter);
+            arguments.add(file.getAbsolutePath());
+        }
+    }
+}
diff --git a/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml b/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml
index f8d6a20..817daa0 100644
--- a/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml
+++ b/src/main/resources/META-INF/m2e/lifecycle-mapping-metadata.xml
@@ -25,6 +25,7 @@
                     <goal>analyse-features</goal>
                     <goal>attach-features</goal>
                     <goal>process-resources</goal>
+                    <goal>launch-features</goal>
                 </goals>
             </pluginExecutionFilter>
             <action>
diff --git a/src/test/java/org/apache/sling/feature/launcher/impl/Main.java b/src/test/java/org/apache/sling/feature/launcher/impl/Main.java
new file mode 100644
index 0000000..00b2db8
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/launcher/impl/Main.java
@@ -0,0 +1,37 @@
+/*
+ * 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.launcher.impl;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+
+public class Main {
+
+    public static final Logger LOGGER = LoggerFactory.getLogger(Main.class);
+
+    public static void main(String[] args) {
+        // Do nothing for now
+        LOGGER.info("main() called with: '{}'", Arrays.asList(args));
+        for(String arg: args) {
+            if("do-fail".equals(arg)) {
+                throw new IllegalArgumentException();
+            }
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/feature/maven/mojos/FeatureLauncherMojoTest.java b/src/test/java/org/apache/sling/feature/maven/mojos/FeatureLauncherMojoTest.java
new file mode 100644
index 0000000..221c779
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/maven/mojos/FeatureLauncherMojoTest.java
@@ -0,0 +1,257 @@
+/*
+ * 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 edu.emory.mathcs.backport.java.util.Arrays;
+import org.apache.maven.model.Build;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugin.MojoFailureException;
+import org.apache.maven.project.MavenProject;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.io.json.FeatureJSONReader;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.internal.util.reflection.Whitebox;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class FeatureLauncherMojoTest {
+
+    private static final String ASSEMBLED_FEATURE_JSON = Feature.class.getName() + "/assembledmain.json";
+
+    private FeatureLauncherMojo mojo = spy(new FeatureLauncherMojo());
+    private Path tempDir;
+
+    @Before
+    public void setup() throws IOException {
+        tempDir = Files.createTempDirectory(getClass().getSimpleName());
+    }
+
+    @Test
+    public void testSimpleFeature() throws MojoExecutionException, IOException, MojoFailureException {
+        final FeatureSelectionConfig cfg = new FeatureSelectionConfig();
+        cfg.setIncludeClassifier("example-runtime");
+        Whitebox.setInternalState(mojo, "selection", cfg);
+        Map<String, Feature> featureMap = new HashMap<>();
+        File featureFile = new File(
+            getClass().getResource("/aggregate-features/test-aggregated-feature-example-runtime.json").getFile());
+        Feature feature = FeatureJSONReader.read(new FileReader(featureFile), null);
+        String cacheKey = ASSEMBLED_FEATURE_JSON + "-cache";
+        featureMap.put(feature.getId().toMvnId(), feature);
+
+        Build mockBuild = mock(Build.class);
+        when(mockBuild.getDirectory()).thenReturn(tempDir.toString());
+
+        MavenProject project = new MavenProject();
+        project.setGroupId("testing");
+        project.setArtifactId("test");
+        project.setVersion("1.0.1");
+        project.setBuild(mockBuild);
+        project.setContextValue(cacheKey, featureMap);
+        mojo.project = project;
+
+        doNothing().when(mojo).checkPreconditions();
+        final List<String> arguments = new ArrayList<>();
+        doAnswer(
+            new Answer() {
+                @Override
+                public Object answer(InvocationOnMock invocation) throws Throwable {
+                    String[] args = (String[]) invocation.getArguments()[0];
+                    arguments.addAll(Arrays.asList(args));
+                    return null;
+                }
+            }
+        ).when(mojo).launch(any(String[].class));
+
+        mojo.execute();
+
+        assertFalse("No Launch Arguments", arguments.isEmpty());
+    }
+
+    @Test
+    public void testFullLaunch() throws MojoFailureException, MojoExecutionException, IOException {
+        final FeatureSelectionConfig cfg = new FeatureSelectionConfig();
+        cfg.setIncludeClassifier("example-runtime");
+        Whitebox.setInternalState(mojo, "selection", cfg);
+        Map<String, Feature> featureMap = new HashMap<>();
+        File featureFile = new File(
+            getClass().getResource("/aggregate-features/test-aggregated-feature-example-runtime.json").getFile());
+        Feature feature = FeatureJSONReader.read(new FileReader(featureFile), null);
+        String cacheKey = ASSEMBLED_FEATURE_JSON + "-cache";
+        featureMap.put(feature.getId().toMvnId(), feature);
+
+        Whitebox.setInternalState(mojo, "artifactClashOverrides", new String[] { "*:*:test" });
+        Whitebox.setInternalState(mojo, "repositoryUrl", "~/.m2/repository");
+        Whitebox.setInternalState(mojo, "frameworkProperties", new String[] { "one=two", "three=four" });
+        Whitebox.setInternalState(mojo, "variableValues", new String[] { "a=b" });
+        Whitebox.setInternalState(mojo, "verbose", true);
+        Whitebox.setInternalState(mojo, "cacheDirectory", new File("./launcher/cache"));
+        Whitebox.setInternalState(mojo, "homeDirectory", new File("./launcher"));
+        Whitebox.setInternalState(mojo, "extensionConfigurations", new String[] { "whatever" });
+        Whitebox.setInternalState(mojo, "frameworkVersion", "1.0.0");
+        Whitebox.setInternalState(mojo, "frameworkArtifacts", new String[] { "next-cool-thing" });
+
+        Build mockBuild = mock(Build.class);
+        when(mockBuild.getDirectory()).thenReturn(tempDir.toString());
+
+        MavenProject project = new MavenProject();
+        project.setGroupId("testing");
+        project.setArtifactId("test");
+        project.setVersion("1.0.1");
+        project.setBuild(mockBuild);
+        project.setContextValue(cacheKey, featureMap);
+        mojo.project = project;
+
+        doNothing().when(mojo).checkPreconditions();
+        final List<String> arguments = new ArrayList<>();
+        doAnswer(
+            new Answer() {
+                @Override
+                public Object answer(InvocationOnMock invocation) throws Throwable {
+                    String[] args = (String[]) invocation.getArguments()[0];
+                    arguments.addAll(Arrays.asList(args));
+                    return null;
+                }
+            }
+        ).when(mojo).launch(any(String[].class));
+
+        mojo.execute();
+
+        assertFalse("No Launch Arguments", arguments.isEmpty());
+    }
+
+    @Test
+    public void testMainReflection() throws MojoFailureException, MojoExecutionException, IOException {
+        final FeatureSelectionConfig cfg = new FeatureSelectionConfig();
+        cfg.setIncludeClassifier("example-runtime");
+        Whitebox.setInternalState(mojo, "selection", cfg);
+        Map<String, Feature> featureMap = new HashMap<>();
+        File featureFile = new File(
+            getClass().getResource("/aggregate-features/test-aggregated-feature-example-runtime.json").getFile());
+        Feature feature = FeatureJSONReader.read(new FileReader(featureFile), null);
+        String cacheKey = ASSEMBLED_FEATURE_JSON + "-cache";
+        featureMap.put(feature.getId().toMvnId(), feature);
+
+        Whitebox.setInternalState(mojo, "artifactClashOverrides", new String[] { "*:*:test" });
+        Build mockBuild = mock(Build.class);
+        when(mockBuild.getDirectory()).thenReturn(tempDir.toString());
+
+        MavenProject project = new MavenProject();
+        project.setGroupId("testing");
+        project.setArtifactId("test");
+        project.setVersion("1.0.1");
+        project.setBuild(mockBuild);
+        project.setContextValue(cacheKey, featureMap);
+        mojo.project = project;
+
+        doNothing().when(mojo).checkPreconditions();
+
+        mojo.execute();
+
+        verify(mojo, times(1)).launch(any(String[].class));
+    }
+
+    @Test
+    public void testMainReflectionFailure() throws MojoFailureException, MojoExecutionException, IOException {
+        final FeatureSelectionConfig cfg = new FeatureSelectionConfig();
+        cfg.setIncludeClassifier("example-runtime");
+        Whitebox.setInternalState(mojo, "selection", cfg);
+        Map<String, Feature> featureMap = new HashMap<>();
+        File featureFile = new File(
+            getClass().getResource("/aggregate-features/test-aggregated-feature-example-runtime.json").getFile());
+        Feature feature = FeatureJSONReader.read(new FileReader(featureFile), null);
+        String cacheKey = ASSEMBLED_FEATURE_JSON + "-cache";
+        featureMap.put(feature.getId().toMvnId(), feature);
+
+        Whitebox.setInternalState(mojo, "artifactClashOverrides", new String[] { "do-fail" });
+        Build mockBuild = mock(Build.class);
+        when(mockBuild.getDirectory()).thenReturn(tempDir.toString());
+
+        MavenProject project = new MavenProject();
+        project.setGroupId("testing");
+        project.setArtifactId("test");
+        project.setVersion("1.0.1");
+        project.setBuild(mockBuild);
+        project.setContextValue(cacheKey, featureMap);
+        mojo.project = project;
+
+        doNothing().when(mojo).checkPreconditions();
+
+        try {
+            mojo.execute();
+            fail("Mojo Execution should have failed");
+        } catch(MojoExecutionException e) {
+            assertTrue("Root Cause Exception was of the wrong type", e.getCause() instanceof IllegalArgumentException);
+        }
+        verify(mojo, times(1)).launch(any(String[].class));
+    }
+
+    @Test
+    public void testFeatureFileReadingIssue() throws MojoFailureException, MojoExecutionException, IOException {
+        final FeatureSelectionConfig cfg = new FeatureSelectionConfig();
+        cfg.setIncludeClassifier("example-runtime");
+        Whitebox.setInternalState(mojo, "selection", cfg);
+        Map<String, Feature> featureMap = new HashMap<>();
+        File featureFile = new File(
+            getClass().getResource("/aggregate-features/test-aggregated-feature-example-runtime.json").getFile());
+        Feature feature = FeatureJSONReader.read(new FileReader(featureFile), null);
+        String cacheKey = ASSEMBLED_FEATURE_JSON + "-cache";
+        featureMap.put(feature.getId().toMvnId(), feature);
+
+        Whitebox.setInternalState(mojo, "artifactClashOverrides", new String[] { "*:*:test" });
+        Build mockBuild = mock(Build.class);
+        when(mockBuild.getDirectory()).thenReturn(tempDir.toString());
+
+        MavenProject project = new MavenProject();
+        project.setGroupId("testing");
+        project.setArtifactId("test");
+        project.setVersion("1.0.1");
+        project.setBuild(mockBuild);
+        project.setContextValue(cacheKey, featureMap);
+        mojo.project = project;
+
+        doNothing().when(mojo).checkPreconditions();
+
+        mojo.execute();
+
+        verify(mojo, times(1)).launch(any(String[].class));
+    }
+}
diff --git a/src/test/resources/aggregate-features/test-aggregated-feature-example-runtime.json b/src/test/resources/aggregate-features/test-aggregated-feature-example-runtime.json
new file mode 100644
index 0000000..497cc18
--- /dev/null
+++ b/src/test/resources/aggregate-features/test-aggregated-feature-example-runtime.json
@@ -0,0 +1,25 @@
+{
+  "id":"org.apache.sling:org.apache.sling.feature.launcher.test:slingosgifeature:example-runtime:1.0.0-SNAPSHOT",
+  "title":"Test Title",
+  "description":"Test Description",
+  "vendor":"The Apache Software Foundation",
+  "license":"Apache License, Version 2.0",
+  "bundles":[
+    {
+      "id":"org.apache.aries:org.apache.aries.util:1.1.3",
+      "start-level":"1"
+    }
+  ],
+  "framework-properties":{
+    "sling.run.mode.install.options":"oak_tar,oak_mongo",
+    "sling.jre.java.xml":",javax.xml;version=\"2.1.0\",javax.xml.datatype;uses:=\"javax.xml.namespace\";version=\"2.1.0\",javax.xml.namespace;version=\"2.1.0\",javax.xml.parsers;uses:=\"javax.xml.validation,org.w3c.dom,org.xml.sax,org.xml.sax.helpers\";version=\"2.1.0\",javax.xml.stream;uses:=\"javax.xml.namespace,javax.xml.stream.events,javax.xml.stream.util,javax.xml.transform\";version=\"1.0.0\",javax.xml.stream.events;uses:=\"javax.xml.namespace,javax.xml.stream\";version=\"1.0.0\",j [...]
+    "felix.systempackages.calculate.uses":"true",
+    "localIndexDir":"${sling.home}/repository/index",
+    "org.osgi.framework.system.packages":"org.osgi.framework;version=\"1.9\",org.osgi.framework.dto;version=\"1.8\";uses:=\"org.osgi.dto\",org.osgi.framework.hooks.bundle;version=\"1.1\";uses:=\"org.osgi.framework\",org.osgi.framework.hooks.resolver;version=\"1.0\";uses:=\"org.osgi.framework.wiring\",org.osgi.framework.hooks.service;version=\"1.1\";uses:=\"org.osgi.framework\",org.osgi.framework.hooks.weaving;version=\"1.1\";uses:=\"org.osgi.framework.wiring\",org.osgi.framework.launch;v [...]
+    "repository.home":"${sling.home}/repository",
+    "felix.systempackages.substitution":"true",
+    "sling.jre-jpms":"{dollar}{felix.jpms.java.base}{dollar}{felix.jpms.java.compiler}{dollar}{felix.jpms.java.datatransfer}{dollar}{felix.jpms.java.desktop}{dollar}{felix.jpms.java.instrument}{dollar}{felix.jpms.java.logging}{dollar}{felix.jpms.java.management}{dollar}{felix.jpms.java.management.rmi}{dollar}{felix.jpms.java.naming}{dollar}{felix.jpms.java.net.http}{dollar}{felix.jpms.java.prefs}{dollar}{felix.jpms.java.rmi}{dollar}{felix.jpms.java.scripting}{dollar}{felix.jpms.java.se}{ [...]
+    "sling.jpms.java.xml":"{dollar}{sling.jre.java.xml},javax.xml.catalog;uses:=\"javax.xml.namespace\";version=\"1.0.0\"",
+    "sling.jre-1.8":",java.applet;version=\"{dollar}{felix.detect.java.version}\",java.awt;version=\"{dollar}{felix.detect.java.version}\",java.awt.color;version=\"{dollar}{felix.detect.java.version}\",java.awt.datatransfer;version=\"{dollar}{felix.detect.java.version}\",java.awt.dnd;version=\"{dollar}{felix.detect.java.version}\",java.awt.event;version=\"{dollar}{felix.detect.java.version}\",java.awt.font;version=\"{dollar}{felix.detect.java.version}\",java.awt.geom;version=\"{dollar}{f [...]
+  }
+}
\ No newline at end of file