Merge pull request #16 from apache/SLING-8251_merged_after_releases

SLING-8251 - Support checking dependencies for content packages
diff --git a/pom.xml b/pom.xml
index 775f419..3fdeb9a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -143,7 +143,33 @@
             <version>1.11.0</version>
             <scope>provided</scope>
         </dependency>
-        
+
+        <!-- Content-Package check -->
+        <dependency>
+            <groupId>org.apache.jackrabbit.vault</groupId>
+            <artifactId>org.apache.jackrabbit.vault</artifactId>
+            <version>3.2.6</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>jackrabbit-spi-commons</artifactId>
+            <version>2.19.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.jcr</groupId>
+            <artifactId>jcr</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.6</version>
+            <scope>provided</scope>
+        </dependency>
+        <!-- END Content-Package check -->
+
         <!-- Testing -->
         <dependency>
             <groupId>junit</groupId>
diff --git a/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckContentPackagesDependencies.java b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckContentPackagesDependencies.java
new file mode 100644
index 0000000..b3ee952
--- /dev/null
+++ b/src/main/java/org/apache/sling/feature/analyser/task/impl/CheckContentPackagesDependencies.java
@@ -0,0 +1,133 @@
+/*
+ * 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.analyser.task.impl;
+
+import java.io.File;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Queue;
+import java.util.Set;
+
+import org.apache.jackrabbit.vault.packaging.Dependency;
+import org.apache.jackrabbit.vault.packaging.PackageId;
+import org.apache.jackrabbit.vault.packaging.PackageManager;
+import org.apache.jackrabbit.vault.packaging.VaultPackage;
+import org.apache.jackrabbit.vault.packaging.impl.PackageManagerImpl;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import org.apache.sling.feature.io.IOUtils;
+import org.apache.sling.feature.scanner.ArtifactDescriptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CheckContentPackagesDependencies implements AnalyserTask {
+
+    private final PackageManager packageManager = new PackageManagerImpl();
+
+    private final Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Override
+    public String getId() {
+        return "content-packages-dependencies";
+    }
+
+    @Override
+    public String getName() {
+        return "Content-packages dependencies checker";
+    }
+
+    @Override
+    public void execute(AnalyserTaskContext ctx) throws Exception {
+        Set<ArtifactDescriptor> descriptors = ctx.getFeatureDescriptor().getArtifactDescriptors();
+        if (descriptors == null || descriptors.isEmpty()) {
+            return;
+        }
+
+        Map<PackageId, Dependency[]> dependenciesMap = new HashMap<>();
+
+        for (ArtifactDescriptor descriptor : descriptors) {
+            onDescriptor(ctx, descriptor, dependenciesMap);
+        }
+
+        for (PackageId contentPackageId : dependenciesMap.keySet()) {
+            verifyDependenciesTree(ctx, contentPackageId, dependenciesMap);
+        }
+    }
+
+    private void onDescriptor(AnalyserTaskContext ctx, ArtifactDescriptor descriptor, Map<PackageId, Dependency[]> dependenciesMap) throws Exception {
+        URL resourceUrl = descriptor.getArtifactFile();
+        File artifactFile = IOUtils.getFileFromURL(resourceUrl, true, null);
+        if (!artifactFile.exists() || !artifactFile.isFile()) {
+            ctx.reportError("Artifact file " + artifactFile + " does not exist or it is not a file");
+            return;
+        }
+
+        try (VaultPackage vaultPackage = packageManager.open(artifactFile, true)) {
+            PackageId packageId = vaultPackage.getId();
+
+            logger.debug("Collecting " + packageId + " dependencies...");
+
+            dependenciesMap.put(packageId, vaultPackage.getDependencies());
+
+            logger.debug(packageId + " dependencies collected.");
+        }
+    }
+
+    private void verifyDependenciesTree(AnalyserTaskContext ctx, PackageId root, Map<PackageId, Dependency[]> dependenciesMap) {
+        logger.debug("Verifying " + root + " transitive dependencies...");
+
+        Queue<Dependency> toBeVisited = new LinkedList<>();
+        enqueue(toBeVisited, dependenciesMap.get(root));
+
+        Set<Dependency> alreadyVisited = new HashSet<>();
+
+        while (!toBeVisited.isEmpty()) {
+            Dependency current = toBeVisited.poll();
+
+            if (alreadyVisited.add(current)) {
+                boolean found = false;
+
+                for (Entry<PackageId, Dependency[]> entry : dependenciesMap.entrySet()) {
+                    if (current.matches(entry.getKey())) {
+                        enqueue(toBeVisited, entry.getValue());
+                        found = true;
+                        break;
+                    }
+                }
+
+                if (!found) {
+                    ctx.reportError("Missing " + current + " dependency for " + root);
+                }
+            }
+        }
+    }
+
+    private static void enqueue(Queue<Dependency> target, Dependency...dependencies) {
+        if (dependencies == null) {
+            return;
+        }
+
+        for (Dependency dependency : dependencies) {
+            target.offer(dependency);
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageScanner.java b/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageScanner.java
index b2f0be1..09f7781 100644
--- a/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageScanner.java
+++ b/src/main/java/org/apache/sling/feature/scanner/impl/ContentPackageScanner.java
@@ -208,27 +208,19 @@
                 }
             }
         } finally {
-            deleteRecursive(tempDir);
+            deleteOnExitRecursive(tempDir);
         }
     }
 
-    private boolean deleteRecursive(File file) {
+    private void deleteOnExitRecursive(File file) {
+        file.deleteOnExit();
         if (file.isDirectory()) {
             File[] childs = file.listFiles();
             if (childs != null) {
                 for (File child : childs) {
-                    if (!deleteRecursive(child)) {
-                        return false;
-                    }
+                    deleteOnExitRecursive(child);
                 }
-                return file.delete();
             }
-            else {
-                return false;
-            }
-        }
-        else {
-            return file.delete();
         }
     }
 
diff --git a/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask b/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask
index 8c8f112..42c29dd 100644
--- a/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask
+++ b/src/main/resources/META-INF/services/org.apache.sling.feature.analyser.task.AnalyserTask
@@ -7,3 +7,4 @@
 org.apache.sling.feature.analyser.task.impl.CheckApiRegionsDependencies
 org.apache.sling.feature.analyser.task.impl.CheckApiRegionsDuplicates
 org.apache.sling.feature.analyser.task.impl.CheckApiRegionsOrder
+org.apache.sling.feature.analyser.task.impl.CheckContentPackagesDependencies
diff --git a/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckContentPackagesDependenciesTest.java b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckContentPackagesDependenciesTest.java
new file mode 100644
index 0000000..a2d961f
--- /dev/null
+++ b/src/test/java/org/apache/sling/feature/analyser/task/impl/CheckContentPackagesDependenciesTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.analyser.task.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.sling.feature.Artifact;
+import org.apache.sling.feature.ArtifactId;
+import org.apache.sling.feature.Feature;
+import org.apache.sling.feature.analyser.task.AnalyserTask;
+import org.apache.sling.feature.analyser.task.AnalyserTaskContext;
+import org.apache.sling.feature.scanner.ArtifactDescriptor;
+import org.apache.sling.feature.scanner.FeatureDescriptor;
+import org.apache.sling.feature.scanner.impl.FeatureDescriptorImpl;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CheckContentPackagesDependenciesTest {
+
+    private AnalyserTask task;
+
+    @Before
+    public void setUp() {
+        task = new CheckContentPackagesDependencies();
+    }
+
+    @After
+    public void tearDown() {
+        task = null;
+    }
+
+    @Test
+    public void dependenciesNotResolvedThrowsError() throws Exception {
+        List<String> errors = execute("test_a-1.0.zip");
+
+        assertFalse(errors.isEmpty());
+
+        Iterator<String> errorsIterator = errors.iterator();
+        assertEquals("Missing my_packages:test_b dependency for my_packages:test_a:1.0", errorsIterator.next());
+        assertEquals("Missing my_packages:test_c:[1.0,2.0) dependency for my_packages:test_a:1.0", errorsIterator.next());
+    }
+
+    @Test
+    public void dependenciesResolved() throws Exception {
+        List<String> errors = execute("test_a-1.0.zip",
+                                      "test_b-1.0.zip",
+                                      "test_c-1.0.zip",
+                                      "test_d-1.0.zip",
+                                      "test_e-1.0.zip");
+        assertTrue(errors.isEmpty());
+    }
+
+    private List<String> execute(String...resources) throws Exception {
+        Feature feature = mock(Feature.class);
+        when(feature.getId()).thenReturn(new ArtifactId("org.apache.sling.testing",
+                                                        "org.apache.sling.testing.contentpackages",
+                                                        "1.0.0",
+                                                        null,
+                                                        null));
+        FeatureDescriptor featureDescriptor = new FeatureDescriptorImpl(feature);
+
+        for (String resource : resources) {
+            ArtifactId id = mock(ArtifactId.class);
+            Artifact artifact = mock(Artifact.class);
+            when(artifact.getId()).thenReturn(id);
+
+            ArtifactDescriptor descriptor = mock(ArtifactDescriptor.class);
+            when(descriptor.getArtifact()).thenReturn(artifact);
+            when(descriptor.getArtifactFile()).thenReturn(getClass().getClassLoader().getResource(resource));
+
+            featureDescriptor.getArtifactDescriptors().add(descriptor);
+        }
+
+        AnalyserTaskContext ctx = mock(AnalyserTaskContext.class);
+
+        when(ctx.getFeatureDescriptor()).thenReturn(featureDescriptor);
+
+        List<String> errors = new LinkedList<>();
+
+        doAnswer(invocation -> {
+            String error = invocation.getArgument(0);
+            errors.add(error);
+            return null;
+        }).when(ctx).reportError(anyString());
+
+        task.execute(ctx);
+
+        return errors;
+    }
+
+}
diff --git a/src/test/resources/test_a-1.0.zip b/src/test/resources/test_a-1.0.zip
new file mode 100644
index 0000000..08df03a
--- /dev/null
+++ b/src/test/resources/test_a-1.0.zip
Binary files differ
diff --git a/src/test/resources/test_b-1.0.zip b/src/test/resources/test_b-1.0.zip
new file mode 100644
index 0000000..85fac13
--- /dev/null
+++ b/src/test/resources/test_b-1.0.zip
Binary files differ
diff --git a/src/test/resources/test_c-1.0.zip b/src/test/resources/test_c-1.0.zip
new file mode 100644
index 0000000..245291a
--- /dev/null
+++ b/src/test/resources/test_c-1.0.zip
Binary files differ
diff --git a/src/test/resources/test_d-1.0.zip b/src/test/resources/test_d-1.0.zip
new file mode 100644
index 0000000..7acf3d1
--- /dev/null
+++ b/src/test/resources/test_d-1.0.zip
Binary files differ
diff --git a/src/test/resources/test_e-1.0.zip b/src/test/resources/test_e-1.0.zip
new file mode 100644
index 0000000..372bbb9
--- /dev/null
+++ b/src/test/resources/test_e-1.0.zip
Binary files differ