Merge branch 'master' into SLING-8569
diff --git a/pom.xml b/pom.xml
index bc50bb7..38bef2a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
   </parent>
 
   <artifactId>org.apache.sling.feature.cpconverter</artifactId>
-  <version>0.0.1-SNAPSHOT</version>
+  <version>0.0.1-T20190704160700-937c5fd</version>
 
   <name>Apache Sling Content Package to Feature Model converter</name>
   <description>Content Package to Feature Model converter tools for Apache Sling</description>
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java b/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
index 83fc5d3..ec874b3 100644
--- a/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
+++ b/src/main/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverter.java
@@ -47,6 +47,7 @@
 import org.apache.sling.feature.cpconverter.handlers.EntryHandlersManager;
 import org.apache.sling.feature.cpconverter.handlers.NodeTypesEntryHandler;
 import org.apache.sling.feature.cpconverter.vltpkg.BaseVaultPackageScanner;
+import org.apache.sling.feature.cpconverter.vltpkg.PackagesEventsEmitter;
 import org.apache.sling.feature.cpconverter.vltpkg.RecollectorVaultPackageScanner;
 import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageAssembler;
 
@@ -76,6 +77,8 @@
 
     private RecollectorVaultPackageScanner recollectorVaultPackageScanner;
 
+    private PackagesEventsEmitter emitter;
+
     public ContentPackage2FeatureModelConverter() {
         this(false);
     }
@@ -126,6 +129,11 @@
         return mainPackageAssembler;
     }
 
+    public ContentPackage2FeatureModelConverter setEmitter(PackagesEventsEmitter emitter) {
+        this.emitter = emitter;
+        return this;
+    }
+
     public void convert(File...contentPackages) throws Exception {
         requireNonNull(contentPackages , "Null content-package(s) can not be converted.");
         secondPass(firstPass(contentPackages));
@@ -166,8 +174,11 @@
     }
 
     protected void secondPass(Collection<VaultPackage> orderedContentPackages) throws Exception {
+        emitter.start();
+
         for (VaultPackage vaultPackage : orderedContentPackages) {
             try {
+                emitter.startPackage(vaultPackage);
                 mainPackageAssembler = VaultPackageAssembler.create(vaultPackage);
                 assemblers.add(mainPackageAssembler);
 
@@ -196,6 +207,7 @@
                 logger.info("Conversion complete!");
 
                 featuresManager.serialize();
+                emitter.endPackage();
             } finally {
                 aclManager.reset();
                 assemblers.clear();
@@ -207,6 +219,8 @@
                 }
             }
         }
+
+        emitter.end();
     }
 
     private void orderDependencies(Map<PackageId, VaultPackage> idFileMap,
@@ -239,6 +253,8 @@
             return;
         }
 
+        emitter.startSubPackage(path, vaultPackage);
+
         ArtifactId packageId = toArtifactId(vaultPackage);
         VaultPackageAssembler clonedPackage = VaultPackageAssembler.create(vaultPackage);
 
@@ -261,6 +277,8 @@
 
         // restore the previous assembler
         mainPackageAssembler = handler;
+
+        emitter.endSubPackage();
     }
 
     protected boolean isSubContentPackageIncluded(String path) {
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 ac4b92b..b1e0d4d 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
@@ -27,6 +27,7 @@
 import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager;
 import org.apache.sling.feature.cpconverter.filtering.RegexBasedResourceFilter;
 import org.apache.sling.feature.cpconverter.handlers.DefaultEntryHandlersManager;
+import org.apache.sling.feature.cpconverter.vltpkg.DefaultPackagesEventsEmitter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -117,7 +118,8 @@
                                                                                                             properties))
                                                              .setBundlesDeployer(new DefaultArtifactsDeployer(artifactsOutputDirectory))
                                                              .setEntryHandlersManager(new DefaultEntryHandlersManager())
-                                                             .setAclManager(new DefaultAclManager());
+                                                             .setAclManager(new DefaultAclManager())
+                                                             .setEmitter(DefaultPackagesEventsEmitter.open(featureModelsOutputDirectory));
 
             if (filteringPatterns != null && filteringPatterns.length > 0) {
                 RegexBasedResourceFilter filter = new RegexBasedResourceFilter();
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DefaultPackagesEventsEmitter.java b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DefaultPackagesEventsEmitter.java
new file mode 100644
index 0000000..30d1d49
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/DefaultPackagesEventsEmitter.java
@@ -0,0 +1,150 @@
+/*
+ * 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.cpconverter.vltpkg;
+
+import static java.util.stream.Collectors.joining;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Date;
+import java.util.Stack;
+
+import org.apache.jackrabbit.vault.fs.api.PathFilterSet;
+import org.apache.jackrabbit.vault.fs.api.WorkspaceFilter;
+import org.apache.jackrabbit.vault.packaging.PackageId;
+import org.apache.jackrabbit.vault.packaging.PackageType;
+import org.apache.jackrabbit.vault.packaging.VaultPackage;
+
+/**
+ * Writes a CSV file <code>containerFile,packageId,packageType[,parentId,pathInParent]</code>
+ */
+public final class DefaultPackagesEventsEmitter implements PackagesEventsEmitter {
+
+    private static final String FILENAME = "content-packages.csv";
+
+    private static final String PATH_SEPARATOR_CHAR = "!";
+
+    public static DefaultPackagesEventsEmitter open(File featureModelsOutputDirectory) throws IOException {
+        if (!featureModelsOutputDirectory.exists()) {
+            featureModelsOutputDirectory.mkdirs();
+        }
+
+        File contentPackagesFiles = new File(featureModelsOutputDirectory, FILENAME);
+        return new DefaultPackagesEventsEmitter(new FileWriter(contentPackagesFiles));
+    }
+
+    private final Stack<String> paths = new Stack<>();
+
+    private final Stack<PackageId> hierarchy = new Stack<>();
+
+    private final PrintWriter writer;
+
+    private VaultPackage current;
+
+    protected DefaultPackagesEventsEmitter(Writer writer) {
+        this.writer = new PrintWriter(writer, true);
+    }
+
+    @Override
+    public void start() {
+        writer.printf("# File created on %s by the Apache Sling Content Package to Sling Feature converter%n", new Date())
+              .printf("# content-package path, content-package ID, content-package type, content-package parent ID, path in parent content-package, absolute path%n");
+    }
+
+    @Override
+    public void end() {
+        writer.close();
+        paths.clear();
+        hierarchy.clear();
+    }
+
+    @Override
+    public void startPackage(VaultPackage vaultPackage) {
+        paths.add(vaultPackage.getFile().getAbsolutePath());
+        hierarchy.add(vaultPackage.getId());
+        current = vaultPackage;
+
+        writer.printf("%s,%s,%s,,,%n",
+                      paths.peek(),
+                      hierarchy.peek(),
+                      detectPackageType(vaultPackage));
+    }
+
+    @Override
+    public void endPackage() {
+        paths.pop();
+        hierarchy.pop();
+    }
+
+    @Override
+    public void startSubPackage(String path, VaultPackage vaultPackage) {
+        paths.add(path);
+        String absolutePath = paths.stream().collect(joining(PATH_SEPARATOR_CHAR));
+
+        writer.printf("%s,%s,%s,%s,%s,%s%n",
+                      current.getFile().getAbsolutePath(),
+                      vaultPackage.getId(),
+                      detectPackageType(vaultPackage),
+                      hierarchy.peek(),
+                      path,
+                      absolutePath);
+
+        hierarchy.add(vaultPackage.getId());
+    }
+
+    @Override
+    public void endSubPackage() {
+        endPackage();
+    }
+
+    private static PackageType detectPackageType(VaultPackage vaultPackage) {
+        PackageType packageType = vaultPackage.getPackageType();
+        if (packageType != null) {
+            return packageType;
+        }
+
+        // borrowed from org.apache.jackrabbit.vault.fs.io.AbstractExporter
+        WorkspaceFilter filter = vaultPackage.getMetaInf().getFilter();
+
+        boolean hasApps = false;
+        boolean hasOther = false;
+        for (PathFilterSet p : filter.getFilterSets()) {
+            if ("cleanup".equals(p.getType())) {
+                continue;
+            }
+            String root = p.getRoot();
+            if ("/apps".equals(root)
+                    || root.startsWith("/apps/")
+                    || "/libs".equals(root)
+                    || root.startsWith("/libs/")) {
+                hasApps = true;
+            } else {
+                hasOther = true;
+            }
+        }
+        if (hasApps && !hasOther) {
+            return PackageType.APPLICATION;
+        } else if (hasOther && !hasApps) {
+            return PackageType.CONTENT;
+        }
+        return PackageType.MIXED;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/PackagesEventsEmitter.java b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/PackagesEventsEmitter.java
new file mode 100644
index 0000000..a854b06
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/cpconverter/vltpkg/PackagesEventsEmitter.java
@@ -0,0 +1,35 @@
+/*
+ * 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.cpconverter.vltpkg;
+
+import org.apache.jackrabbit.vault.packaging.VaultPackage;
+
+public interface PackagesEventsEmitter {
+
+    void start();
+
+    void end();
+
+    void startPackage(VaultPackage vaultPackage);
+
+    void endPackage();
+
+    void startSubPackage(String path, VaultPackage vaultPackage);
+
+    void endSubPackage();
+
+}
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 617a6c2..f8808c7 100644
--- a/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
+++ b/src/test/java/org/apache/sling/feature/cpconverter/ContentPackage2FeatureModelConverterTest.java
@@ -47,6 +47,7 @@
 import org.apache.sling.feature.cpconverter.features.DefaultFeaturesManager;
 import org.apache.sling.feature.cpconverter.filtering.RegexBasedResourceFilter;
 import org.apache.sling.feature.cpconverter.handlers.DefaultEntryHandlersManager;
+import org.apache.sling.feature.cpconverter.vltpkg.DefaultPackagesEventsEmitter;
 import org.apache.sling.feature.io.json.FeatureJSONReader;
 import org.junit.After;
 import org.junit.Before;
@@ -117,6 +118,7 @@
 
         converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null))
                  .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                 .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                  .convert(packageFile);
 
         verifyFeatureFile(outputDirectory,
@@ -274,6 +276,7 @@
 
         converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null))
                  .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                 .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                  .convert(packageFile);
     }
 
@@ -294,6 +297,7 @@
 
         converter.setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
                  .setFeaturesManager(new DefaultFeaturesManager(false, 5, outputDirectory, null, null))
+                 .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                  .convert(packageFile);
 
         String pid = "this.is.just.a.pid";
@@ -311,6 +315,7 @@
         String overrideId = "${project.groupId}:${project.artifactId}:slingosgifeature:asd.test.all-1.0.0:${project.version}";
         converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, overrideId, null))
                  .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                 .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                  .convert(packageFile);
 
         verifyFeatureFile(outputDirectory,
@@ -378,6 +383,7 @@
 
         converter.setFeaturesManager(new DefaultFeaturesManager(true, 5, outputDirectory, null, null))
                  .setBundlesDeployer(new DefaultArtifactsDeployer(outputDirectory))
+                 .setEmitter(DefaultPackagesEventsEmitter.open(outputDirectory))
                  .convert(contentPackages[0]);
 
         File featureFile = new File(outputDirectory, "test_a.json");
diff --git a/src/test/java/org/apache/sling/feature/cpconverter/vltpkg/PackagesEventsEmitterTest.java b/src/test/java/org/apache/sling/feature/cpconverter/vltpkg/PackagesEventsEmitterTest.java
new file mode 100644
index 0000000..7bf35bf
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/cpconverter/vltpkg/PackagesEventsEmitterTest.java
@@ -0,0 +1,77 @@
+/*
+ * 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.cpconverter.vltpkg;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.StringWriter;
+
+import org.apache.jackrabbit.vault.packaging.PackageId;
+import org.apache.jackrabbit.vault.packaging.PackageType;
+import org.apache.jackrabbit.vault.packaging.VaultPackage;
+import org.junit.Test;
+
+public class PackagesEventsEmitterTest {
+
+    @Test
+    public void justCheckEmissions() {
+        VaultPackage parent = mock(VaultPackage.class);
+        when(parent.getPackageType()).thenReturn(PackageType.MIXED);
+        when(parent.getId()).thenReturn(new PackageId("apache/sling", "parent", "1.0.0"));
+        when(parent.getFile()).thenReturn(new File("/org/apache/sling/content-package.zip"));
+
+        StringWriter stringWriter = new StringWriter();
+        PackagesEventsEmitter emitter = new DefaultPackagesEventsEmitter(stringWriter);
+        emitter.start();
+        emitter.startPackage(parent);
+
+        VaultPackage contentChild = mock(VaultPackage.class);
+        when(contentChild.getPackageType()).thenReturn(PackageType.CONTENT);
+        when(contentChild.getId()).thenReturn(new PackageId("apache/sling", "content-child", "1.0.0"));
+        emitter.startSubPackage("/jcr_root/etc/packages/org/apache/sling/content-child-1.0.zip", contentChild);
+        emitter.endSubPackage();
+
+        VaultPackage applicationChild = mock(VaultPackage.class);
+        when(applicationChild.getPackageType()).thenReturn(PackageType.APPLICATION);
+        when(applicationChild.getId()).thenReturn(new PackageId("apache/sling", "application-child", "1.0.0"));
+        emitter.startSubPackage("/jcr_root/etc/packages/org/apache/sling/application-child-1.0.zip", applicationChild);
+
+        VaultPackage nestedChild = mock(VaultPackage.class);
+        when(nestedChild.getPackageType()).thenReturn(PackageType.CONTAINER);
+        when(nestedChild.getId()).thenReturn(new PackageId("apache/sling", "nested-child", "1.0.0"));
+        emitter.startSubPackage("/jcr_root/etc/packages/org/apache/sling/nested-child-1.0.zip", nestedChild);
+        emitter.endSubPackage();
+
+        // applicationChild
+        emitter.endSubPackage();
+
+        emitter.endPackage();
+        emitter.end();
+
+        String actual = stringWriter.toString();
+
+        String expected = "/org/apache/sling/content-package.zip,apache/sling:parent:1.0.0,MIXED,,,\n" + 
+                "/org/apache/sling/content-package.zip,apache/sling:content-child:1.0.0,CONTENT,apache/sling:parent:1.0.0,/jcr_root/etc/packages/org/apache/sling/content-child-1.0.zip,/org/apache/sling/content-package.zip!/jcr_root/etc/packages/org/apache/sling/content-child-1.0.zip\n" + 
+                "/org/apache/sling/content-package.zip,apache/sling:application-child:1.0.0,APPLICATION,apache/sling:parent:1.0.0,/jcr_root/etc/packages/org/apache/sling/application-child-1.0.zip,/org/apache/sling/content-package.zip!/jcr_root/etc/packages/org/apache/sling/application-child-1.0.zip\n" + 
+                "/org/apache/sling/content-package.zip,apache/sling:nested-child:1.0.0,CONTAINER,apache/sling:application-child:1.0.0,/jcr_root/etc/packages/org/apache/sling/nested-child-1.0.zip,/org/apache/sling/content-package.zip!/jcr_root/etc/packages/org/apache/sling/application-child-1.0.zip!/jcr_root/etc/packages/org/apache/sling/nested-child-1.0.zip\n";
+        assertTrue(actual.endsWith(expected));
+    }
+
+}