SLING-7683 Support run modes in separate files

When the following variable is defined in a feature model:
  "provisioning.runmode": "comma,separated,list"
Then all the bundles, configurations and extensions in this model have
the specified runmode(s) when converted into the provisioning model.
diff --git a/src/main/java/org/apache/sling/feature/modelconverter/FeatureToProvisioning.java b/src/main/java/org/apache/sling/feature/modelconverter/FeatureToProvisioning.java
index 6754456..afaa2e2 100644
--- a/src/main/java/org/apache/sling/feature/modelconverter/FeatureToProvisioning.java
+++ b/src/main/java/org/apache/sling/feature/modelconverter/FeatureToProvisioning.java
@@ -59,6 +59,7 @@
 public class FeatureToProvisioning {
     private static final Logger LOGGER = LoggerFactory.getLogger(FeatureToProvisioning.class);
     static final String PROVISIONING_MODEL_NAME_VARIABLE = "provisioning.model.name";
+    static final String PROVISIONING_RUNMODE = "provisioning.runmode";
 
     public static void convert(File inputFile, File outputFile, ArtifactManager am) throws IOException {
         if (outputFile.exists()) {
@@ -78,9 +79,15 @@
             featureName = feature.getId().getArtifactId();
         }
 
+        String runMode = (String) feature.getVariables().remove(PROVISIONING_RUNMODE);
+        String[] runModes = null;
+        if (runMode != null) {
+            runModes = runMode.split(",");
+        }
+
         Feature newFeature = new Feature(featureName);
         convert(newFeature, feature.getVariables(), feature.getBundles(), feature.getConfigurations(),
-                feature.getFrameworkProperties(), feature.getExtensions(), outputFile.getAbsolutePath());
+                feature.getFrameworkProperties(), feature.getExtensions(), outputFile.getAbsolutePath(), runModes);
     }
 
     /*
@@ -117,11 +124,15 @@
         }
         final Feature feature = new Feature(featureName);
 
-        convert(feature, app.getVariables(), app.getBundles(), app.getConfigurations(), app.getFrameworkProperties(), app.getExtensions(), outputFile);
+        convert(feature, app.getVariables(), app.getBundles(), app.getConfigurations(),
+                app.getFrameworkProperties(), app.getExtensions(), outputFile, null);
     }
 
     private static void convert(Feature f, KeyValueMap variables, Bundles bundles, Configurations configurations, KeyValueMap frameworkProps,
-            Extensions extensions, String outputFile) {
+            Extensions extensions, String outputFile, String [] runModes) {
+        if (runModes != null && runModes.length == 0) {
+            runModes = null;
+        }
         org.apache.sling.provisioning.model.KeyValueMap<String> vars = f.getVariables();
         for (Map.Entry<String, String> entry : variables) {
             vars.put(entry.getKey(), entry.getValue());
@@ -162,8 +173,11 @@
                 startLevel = 20;
             }
 
-            String[] runModes = getRunModes(bundle);
-            f.getOrCreateRunMode(runModes).getOrCreateArtifactGroup(startLevel).add(newBundle);
+            String[] bundleRunModes = runModes;
+            if (bundleRunModes == null) {
+                bundleRunModes = getRunModes(bundle);
+            }
+            f.getOrCreateRunMode(bundleRunModes).getOrCreateArtifactGroup(startLevel).add(newBundle);
         }
 
         // configurations
@@ -188,11 +202,13 @@
                 }
                 c.getProperties().put(key, val);
             }
-
-            String[] runModes = runModeList.toArray(new String[] {});
-            if (runModes.length == 0)
-                runModes = null;
-            f.getOrCreateRunMode(runModes).getConfigurations().add(c);
+            String[] cfgRunModes = runModes;
+            if (cfgRunModes == null) {
+                cfgRunModes = runModeList.toArray(new String[] {});
+                if (cfgRunModes.length == 0)
+                    cfgRunModes = null;
+            }
+            f.getOrCreateRunMode(cfgRunModes).getConfigurations().add(c);
         }
 
         // framework properties
@@ -214,17 +230,19 @@
         for(final Extension ext : extensions) {
             if ( FeatureConstants.EXTENSION_NAME_CONTENT_PACKAGES.equals(ext.getName()) ) {
                 for(final org.apache.sling.feature.Artifact cp : ext.getArtifacts() ) {
-                    String[] runmodes = null;
+                    String[] extRunModes = runModes;
                     final ArtifactId id = cp.getId();
                     final Artifact newCP = new Artifact(id.getGroupId(), id.getArtifactId(), id.getVersion(), id.getClassifier(), id.getType());
                     for(final Map.Entry<String, String> prop : cp.getMetadata()) {
                         if (prop.getKey().equals("runmodes")) {
-                            runmodes = prop.getValue().split(",");
+                            if (extRunModes == null) {
+                                extRunModes = prop.getValue().split(",");
+                            }
                         } else {
                             newCP.getMetadata().put(prop.getKey(), prop.getValue());
                         }
                     }
-                    f.getOrCreateRunMode(runmodes).getOrCreateArtifactGroup(20).add(newCP);
+                    f.getOrCreateRunMode(extRunModes).getOrCreateArtifactGroup(20).add(newCP);
                 }
 
             } else if ( FeatureConstants.EXTENSION_NAME_REPOINIT.equals(ext.getName()) ) {
diff --git a/src/test/java/org/apache/sling/feature/modelconverter/ModelConverterTest.java b/src/test/java/org/apache/sling/feature/modelconverter/ModelConverterTest.java
index ccdcd59..0e9fd87 100644
--- a/src/test/java/org/apache/sling/feature/modelconverter/ModelConverterTest.java
+++ b/src/test/java/org/apache/sling/feature/modelconverter/ModelConverterTest.java
@@ -113,6 +113,16 @@
     }
 
     @Test
+    public void testSeparateRunmodeFiles() throws Exception {
+        testConvertToProvisioningModel(
+            new String[] {
+                    "/runmodeseparation/oak_no_runmode.json",
+                    "/runmodeseparation/oak_mongo.json",
+                    "/runmodeseparation/oak_tar.json"},
+            "/oak.txt");
+    }
+
+    @Test
     public void testOakToFeature() throws Exception {
         testConvertToFeature("/oak.txt", "/oak.json");
     }
@@ -298,6 +308,22 @@
         assertModelsEqual(expected, actual);
     }
 
+    public void testConvertToProvisioningModel(String [] jsonFiles, String expectedProvModel) throws URISyntaxException, IOException {
+        List<File> generatedFiles = new ArrayList<>();
+        for (String jsonFile : jsonFiles) {
+            File inFile = new File(getClass().getResource(jsonFile).toURI());
+            File outFile = new File(tempDir.toFile(), inFile.getName() + ".txt.generated");
+
+            FeatureToProvisioning.convert(inFile, outFile, artifactManager);
+            generatedFiles.add(outFile);
+        }
+
+        File expectedFile = new File(getClass().getResource(expectedProvModel).toURI());
+        Model expected = readProvisioningModel(expectedFile);
+        Model actual = readProvisioningModel(generatedFiles);
+        assertModelsEqual(expected, actual);
+    }
+
     private static Model readProvisioningModel(File modelFile) throws IOException {
         return readProvisioningModel(Collections.singletonList(modelFile));
     }
diff --git a/src/test/resources/runmodeseparation/oak_mongo.json b/src/test/resources/runmodeseparation/oak_mongo.json
new file mode 100644
index 0000000..9ed9a8e
--- /dev/null
+++ b/src/test/resources/runmodeseparation/oak_mongo.json
@@ -0,0 +1,29 @@
+{
+    "id": "generated/oak_mongo/1.0.0",
+    
+    "variables": {
+        "oak.version": "1.6.8",
+        "provisioning.model.name": "oak",
+        "provisioning.runmode": "oak_mongo"
+    },
+
+    "bundles": [
+        {
+            "id": "org.mongodb/mongo-java-driver/3.4.1",
+            "start-level": 15,
+            "run-modes": "oak_mongo"
+        },
+        {
+            "id": "com.h2database/h2-mvstore/1.4.196",
+            "start-level": 15,
+            "run-modes": "oak_mongo"
+        }
+    ],
+    
+    "configurations": {
+        "org.apache.jackrabbit.oak.plugins.document.DocumentNodeStoreService": {
+            "mongouri": "mongodb://localhost:27017",
+            "db": "sling"
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/resources/runmodeseparation/oak_no_runmode.json b/src/test/resources/runmodeseparation/oak_no_runmode.json
new file mode 100644
index 0000000..abef0fc
--- /dev/null
+++ b/src/test/resources/runmodeseparation/oak_no_runmode.json
@@ -0,0 +1,76 @@
+{
+    "id": "generated/oak_no_runmode/1.0.0",
+    
+    "variables": {
+        "oak.version": "1.6.8",
+        "provisioning.model.name": "oak"
+    },
+    
+    "bundles": [
+        {
+            "id": "org.apache.felix/org.apache.felix.jaas/1.0.2",
+            "start-level": 10
+        },
+        {
+            "id": "org.apache.jackrabbit/oak-core/${oak.version}",
+            "start-level": 15
+        },
+        {
+            "id": "org.apache.jackrabbit/oak-commons/${oak.version}",
+            "start-level": 15
+        },
+        {
+            "id": "org.apache.jackrabbit/oak-lucene/${oak.version}",
+            "start-level": 15
+        },
+        {
+            "id": "org.apache.jackrabbit/oak-blob/${oak.version}",
+            "start-level": 15
+        },
+        {
+            "id": "org.apache.jackrabbit/oak-jcr/${oak.version}",
+            "start-level": 15
+        },
+        {
+            "id": "org.apache.sling/org.apache.sling.jcr.oak.server/1.1.4",
+            "start-level": 16
+        }
+    ],
+    "configurations": {
+        "org.apache.felix.jaas.Configuration.factory~GuestLoginModule": {
+            "jaas.controlFlag": "optional",
+            "jaas.classname": "org.apache.jackrabbit.oak.spi.security.authentication.GuestLoginModule",
+            "jaas.ranking:Integer": 300
+        }, 
+        "org.apache.felix.jaas.Configuration.factory~LoginModuleImpl" : {
+            "jaas.controlFlag": "required",
+            "jaas.classname": "org.apache.jackrabbit.oak.security.authentication.user.LoginModuleImpl"
+        },
+        "org.apache.felix.jaas.Configuration.factory~TokenLoginModule" : {
+            "jaas.controlFlag": "sufficient",
+            "jaas.classname": "org.apache.jackrabbit.oak.security.authentication.token.TokenLoginModule",
+            "jaas.ranking:Integer": 200
+        },
+        "org.apache.felix.jaas.ConfigurationSpi": {
+            "jaas.defaultRealmName": "jackrabbit.oak",
+            "jaas.configProviderName": "FelixJaasProvider"
+        },
+        "org.apache.jackrabbit.oak.security.authentication.AuthenticationConfigurationImpl" : {
+            "org.apache.jackrabbit.oak.authentication.configSpiName": "FelixJaasProvider"
+        },
+        "org.apache.jackrabbit.oak.security.user.UserConfigurationImpl": {
+            "groupsPath": "/home/groups",
+            "usersPath": "/home/users",
+            "defaultDepth": "1",
+            "importBehavior": "besteffort"
+        },
+        "org.apache.jackrabbit.oak.security.user.RandomAuthorizableNodeName": {
+            "length:Integer": 21
+        },
+        "org.apache.jackrabbit.oak.spi.security.user.action.DefaultAuthorizableActionProvider": {
+            "enabledActions": ["org.apache.jackrabbit.oak.spi.security.user.action.AccessControlAction"],
+            "userPrivilegeNames": ["jcr:all"],
+            "groupPrivilegeNames": ["jcr:read"]
+        }
+    }
+}
diff --git a/src/test/resources/runmodeseparation/oak_tar.json b/src/test/resources/runmodeseparation/oak_tar.json
new file mode 100644
index 0000000..71ade48
--- /dev/null
+++ b/src/test/resources/runmodeseparation/oak_tar.json
@@ -0,0 +1,23 @@
+{
+    "id": "generated/oak_tar/1.0.0",
+    
+    "variables": {
+        "oak.version": "1.6.8",
+        "provisioning.model.name": "oak",
+        "provisioning.runmode": "oak_tar"
+    },
+    
+    "bundles": [
+        {
+            "id": "org.apache.jackrabbit/oak-segment-tar/${oak.version}",
+            "start-level": 15,
+            "run-modes": "oak_tar"
+        }
+    ],
+    
+    "configurations": {
+        "org.apache.jackrabbit.oak.segment.SegmentNodeStoreService.runmodes.oak_tar": {
+            "name": "Default NodeStore"
+        }
+    }
+}
\ No newline at end of file