Merge remote-tracking branch 'origin/master' into issues/SLING-8630
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/cli/ContentPackage2FeatureModelConverterLauncher.java b/src/main/java/org/apache/sling/feature/cpconverter/cli/ContentPackage2FeatureModelConverterLauncher.java
index b1e0d4d..faf0403 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/cli/ContentPackage2FeatureModelConverterLauncher.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/cli/ContentPackage2FeatureModelConverterLauncher.java
@@ -18,6 +18,7 @@
 
 import java.io.File;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.TimeZone;
 
@@ -76,6 +77,12 @@
     @Option(names = { "-i", "--artifact-id" }, description = "The optional Artifact Id the Feature File will have, once generated; it will be derived, if not specified.", required = false)
     private String artifactIdOverride;
 
+    @Option(names = { "-p", "--fm-prefix" }, description = "The optional prefix of the output file", required = false)
+    private String fmPrefix;
+
+    @Option(names = { "-r", "--api-region" }, description = "The API Regions assigned to the generated features", required = false)
+    private List<String> apiRegions;
+
     @Option(names = {"-D", "--define"}, description = "Define a system property", required = false)
     private Map<String, String> properties = new HashMap<>();
 
@@ -110,12 +117,17 @@
         logger.info("");
 
         try {
+            DefaultFeaturesManager featuresManager = new DefaultFeaturesManager(mergeConfigurations,
+                                                            bundlesStartOrder,
+                                                            featureModelsOutputDirectory,
+                                                            artifactIdOverride,
+                                                            fmPrefix,
+                                                            properties);
+            if (apiRegions != null)
+                featuresManager.setAPIRegions(apiRegions);
+
             ContentPackage2FeatureModelConverter converter = new ContentPackage2FeatureModelConverter(strictValidation)
-                                                             .setFeaturesManager(new DefaultFeaturesManager(mergeConfigurations,
-                                                                                                            bundlesStartOrder,
-                                                                                                            featureModelsOutputDirectory,
-                                                                                                            artifactIdOverride,
-                                                                                                            properties))
+                                                             .setFeaturesManager(featuresManager)
                                                              .setBundlesDeployer(new DefaultArtifactsDeployer(artifactsOutputDirectory))
                                                              .setEntryHandlersManager(new DefaultEntryHandlersManager())
                                                              .setAclManager(new DefaultAclManager())
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java b/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java
index 1a16252..1f13de6 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/features/DefaultFeaturesManager.java
@@ -21,10 +21,12 @@
 
 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;
 
@@ -66,31 +68,58 @@
 
     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);
+        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();
+
         runModes.clear();
     }
 
+    private void initAPIRegions() {
+        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());
+            targetFeature.getExtensions().add(apiRegions);
+        }
+    }
+
     public Feature getTargetFeature() {
         return targetFeature;
     }
@@ -201,7 +230,9 @@
     }
 
     private void seralize(Feature feature, String runMode, RunmodeMapper runmodeMapper) throws Exception {
-        StringBuilder fileNameBuilder = new StringBuilder().append(feature.getId().getArtifactId());
+        StringBuilder fileNameBuilder = new StringBuilder()
+            .append((prefix != null) ? prefix : "")
+            .append(feature.getId().getArtifactId());
 
         String classifier = feature.getId().getClassifier();
         if (classifier != null && !classifier.isEmpty()) {
@@ -238,4 +269,9 @@
         }
     }
 
+    public synchronized DefaultFeaturesManager setAPIRegions(List<String> regions) {
+        targetAPIRegions.clear();
+        targetAPIRegions.addAll(regions);
+        return this;
+    }
 }
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java b/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
index bbe9197..6eeb8ae 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
@@ -24,11 +24,16 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileReader;
+import java.io.IOException;
 import java.io.Reader;
+import java.io.StringReader;
 import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.List;
@@ -42,6 +47,7 @@
 import org.apache.sling.feature.ArtifactId;
 import org.apache.sling.feature.Artifacts;
 import org.apache.sling.feature.Extension;
+import org.apache.sling.feature.ExtensionType;
 import org.apache.sling.feature.Feature;
 import org.apache.sling.feature.cpconverter.acl.DefaultAclManager;
 import org.apache.sling.feature.cpconverter.artifacts.DefaultArtifactsDeployer;
@@ -54,15 +60,19 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+
 public class ContentPackage2FeatureModelConverterTest {
 
     /**
      * Test package A-1.0. Depends on B and C-1.X
      * Test package B-1.0. Depends on C
      */
-    private static String[] TEST_PACKAGES_INPUT = { "test_c-1.0.zip", "test_a-1.0.zip", "test_b-1.0.zip" }; 
+    private static String[] TEST_PACKAGES_INPUT = { "test_c-1.0.zip", "test_a-1.0.zip", "test_b-1.0.zip" };
 
-    private static String[] TEST_PACKAGES_OUTPUT = { "my_packages:test_c:1.0", "my_packages:test_b:1.0", "my_packages:test_a:1.0" }; 
+    private static String[] TEST_PACKAGES_OUTPUT = { "my_packages:test_c:1.0", "my_packages:test_b:1.0", "my_packages:test_a:1.0" };
 
     private static String[] TEST_PACKAGES_CYCLIC_DEPENDENCY = { "test_d-1.0.zip",
                                                                 "test_c-1.0.zip",
@@ -117,7 +127,7 @@
 
         File outputDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + '_' + System.currentTimeMillis());
 
-        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null))
+        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null))
                  .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
                  .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                  .convert(packageFile);
@@ -196,7 +206,7 @@
                              "jcr_root/config.xml",
                              "jcr_root/definition/.content.xml");
     }
-    
+
     @Test
     public void convertContentPackageDropContent() throws Exception {
         URL packageUrl = getClass().getResource("test-content-package.zip");
@@ -204,7 +214,7 @@
 
         File outputDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + '_' + System.currentTimeMillis());
 
-        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null))
+        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null))
                  .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
                  .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                  .setDropContent(true)
@@ -267,6 +277,53 @@
         assertFalse(new File(outputDirectory, "asd/sample/asd.retail.all/0.0.1/asd.retail.all-0.0.1-cp2fm-converted.zip").exists());
     }
 
+    @Test
+    public void testConvertContentPackageWithAPIRegion() throws Exception {
+        URL cp = getClass().getResource("test_c-1.0.zip");
+        File cpFile = new File(cp.getFile());
+        File outDir = Files.createTempDirectory(getClass().getSimpleName()).toFile();
+
+        try {
+            DefaultFeaturesManager fm = new DefaultFeaturesManager(true, 5, outDir, null, null, null);
+            fm.setAPIRegions(Arrays.asList("global", "foo.bar"));
+            converter.setFeaturesManager(fm)
+                     .setBundlesDeployer(new DefaultArtifactsDeployer(outDir))
+                     .setEmitter(DefaultPackagesEventsEmitter.open(outDir))
+                     .convert(cpFile);
+
+            File featureFile = new File(outDir, "test_c.json");
+            try (Reader reader = new FileReader(featureFile)) {
+                Feature feature = FeatureJSONReader.read(reader, featureFile.getAbsolutePath());
+
+                Extension apiRegions = feature.getExtensions().getByName("api-regions");
+                assertEquals(ExtensionType.JSON, apiRegions.getType());
+                String json = apiRegions.getJSON();
+                JsonArray ja = Json.createReader(new StringReader(json)).readArray();
+                assertEquals(2, ja.size());
+
+                JsonObject globalJO = ja.getJsonObject(0);
+                assertEquals("global", globalJO.getString("name"));
+                assertEquals(0, globalJO.getJsonArray("exports").size());
+
+                JsonObject foobarJO = ja.getJsonObject(1);
+                assertEquals("foo.bar", foobarJO.getString("name"));
+                assertEquals(0, foobarJO.getJsonArray("exports").size());
+            }
+
+        } finally {
+            deleteDirTree(outDir);
+        }
+    }
+
+    private void deleteDirTree(File dir) throws IOException {
+        Path tempDir = dir.toPath();
+
+        Files.walk(tempDir)
+            .sorted(Comparator.reverseOrder())
+            .map(Path::toFile)
+            .forEach(File::delete);
+    }
+
     private void verifyFeatureFile(File outputDirectory,
                                    String name,
                                    String expectedArtifactId,
@@ -350,7 +407,7 @@
 
         File outputDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + '_' + System.currentTimeMillis());
 
-        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null))
+        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null))
                  .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
                  .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                  .convert(packageFile);
@@ -372,7 +429,7 @@
         File packageFile = FileUtils.toFile(packageUrl);
 
         converter.setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
-                 .setFeaturesManager(new DefaultFeaturesManager(false, 5, outputDirectory, null, null))
+                 .setFeaturesManager(new DefaultFeaturesManager(false, 5, outputDirectory, null, null, null))
                  .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                  .convert(packageFile);
 
@@ -389,7 +446,7 @@
         File outputDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + '_' + System.currentTimeMillis());
 
         String overrideId = "${project.groupId}:${project.artifactId}:slingosgifeature:asd.test.all-1.0.0:${project.version}";
-        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, overrideId, null))
+        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, overrideId, null, null))
                  .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
                  .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                  .convert(packageFile);
@@ -457,7 +514,7 @@
 
         File outputDirectory = new File(System.getProperty("java.io.tmpdir"), getClass().getName() + '_' + System.currentTimeMillis());
 
-        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null))
+        converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null, null))
                  .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
                  .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                  .convert(contentPackages[0]);
@@ -471,18 +528,18 @@
 
             String expected = "register nodetypes\n" +
                     "<<===\n" +
-                    "<< <'sling'='http://sling.apache.org/jcr/sling/1.0'>\n" + 
-                    "<< <'nt'='http://www.jcp.org/jcr/nt/1.0'>\n" + 
-                    "<< <'rep'='internal'>\n" + 
-                    "\n" + 
-                    "<< [sling:Folder] > nt:folder\n" + 
-                    "<<   - * (undefined) multiple\n" + 
-                    "<<   - * (undefined)\n" + 
-                    "<<   + * (nt:base) = sling:Folder version\n" + 
-                    "\n" + 
-                    "<< [rep:RepoAccessControllable]\n" + 
-                    "<<   mixin\n" + 
-                    "<<   + rep:repoPolicy (rep:Policy) protected ignore\n" + 
+                    "<< <'sling'='http://sling.apache.org/jcr/sling/1.0'>\n" +
+                    "<< <'nt'='http://www.jcp.org/jcr/nt/1.0'>\n" +
+                    "<< <'rep'='internal'>\n" +
+                    "\n" +
+                    "<< [sling:Folder] > nt:folder\n" +
+                    "<<   - * (undefined) multiple\n" +
+                    "<<   - * (undefined)\n" +
+                    "<<   + * (nt:base) = sling:Folder version\n" +
+                    "\n" +
+                    "<< [rep:RepoAccessControllable]\n" +
+                    "<<   mixin\n" +
+                    "<<   + rep:repoPolicy (rep:Policy) protected ignore\n" +
                     "\n===>>\n";
             String actual = repoinitExtension.getText();
             assertEquals(expected, actual);