SLING-5294 - TeleporterRule.withResources(...) implemented, with integration tests

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1715072 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index 7b4c49e..1cf9a95 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,7 +31,7 @@
   <packaging>jar</packaging>
   <name>Apache Sling JUnit Tests Teleporter</name>
   <description>Client-side implementation of the Teleporter mechanism for server-side JUnit tests</description>
-
+  
   <dependencies>
     <dependency>
       <groupId>junit</groupId>
@@ -54,6 +54,12 @@
       <version>1.0.13-SNAPSHOT</version>
     </dependency>
     <dependency>
+      <groupId>commons-io</groupId>
+      <artifactId>commons-io</artifactId>
+      <version>2.4</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
     </dependency>
diff --git a/src/main/java/org/apache/sling/testing/teleporter/client/ClassResourceVisitor.java b/src/main/java/org/apache/sling/testing/teleporter/client/ClassResourceVisitor.java
new file mode 100644
index 0000000..7373fee
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/teleporter/client/ClassResourceVisitor.java
@@ -0,0 +1,127 @@
+/*
+ * 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.testing.teleporter.client;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+/** Finds and visits resources provided by ClassLoaders based
+ *  on their path. If a path points to a folder, its child
+ *  resources are also visited. Currently works for the "file:"
+ *  and "jar:" resource protocols. 
+ */
+public class ClassResourceVisitor {
+    
+    private final Class<?> clazz;
+    private final String path;
+    
+    static interface Processor {
+        void process(String resourcePath, InputStream resourceStream) throws IOException;
+    }
+    
+    /** Visit the resources provided by clazz's ClassLoader,
+     *  based on the supplied path.
+     *  
+     * @param clazz
+     * @param path If that points to a folder, it is visited recursively
+     */
+    public ClassResourceVisitor(Class<?> clazz, String path) {
+        this.clazz = clazz;
+        this.path = path;
+    }
+    
+    public void visit(Processor p) throws IOException {
+        final URL resourceURL = clazz.getResource(path);
+        if(resourceURL == null) {
+            return;
+        }
+        
+        final String protocol = resourceURL.getProtocol();
+        
+        if("file".equals(protocol)) {
+            // Get base path and remove ending slash
+            String basePath = clazz.getResource("/").getPath();
+            basePath = basePath.substring(0, basePath.length() - 1);
+            processFile(basePath, new File(resourceURL.getPath()), p);
+            
+        } else if("jar".equals(protocol)) {
+            // Jar entries use relative paths
+            final String rPath = path.startsWith("/") ? path.substring(1) : path;
+            final String jarFilePath = resourceURL.getPath().split("!")[0].substring("file:".length());
+            final JarFile jar = new JarFile(jarFilePath);
+            try {
+                final Enumeration<JarEntry> entries = jar.entries();
+                while (entries.hasMoreElements()) {
+                    final JarEntry e = entries.nextElement();
+                    if(!e.isDirectory() && jarEntryMatches(rPath, e.getName())) {
+                        final InputStream is = jar.getInputStream(e);
+                        try {
+                            p.process("/" + e.getName(), is);
+                        } finally {
+                            is.close();
+                        }
+                    }
+                }
+            } finally {
+                jar.close();
+            }
+           
+            
+        } else {
+            throw new IllegalArgumentException("Unkown protocol " + protocol);
+        }
+    }
+    
+    /** True if the entry part matches the given path.
+     * @param givenPath if it ends with / it's considered a folder name, otherwise
+     *              we check for an exact match
+     * @param entryPath the path of the jar entry to check
+     * @return true if there's a match
+     */
+    private boolean jarEntryMatches(String givenPath, String entryPath) {
+        if(givenPath.endsWith("/")) {
+            return entryPath.startsWith(givenPath);
+        } else {
+            return entryPath.equals(givenPath);
+        }
+    }
+    
+    private void processFile(String basePath, File f, Processor p) throws IOException {
+        if(f.isDirectory()) {
+            final String [] names = f.list();
+            if(names != null) {
+                for(String name : names) {
+                    processFile(basePath, new File(f, name), p);
+                }
+            }
+        } else {
+            final InputStream is = new BufferedInputStream(new FileInputStream(f));
+            try {
+                p.process(f.getAbsolutePath().substring(basePath.length()), is);
+            } finally {
+                is.close();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/teleporter/client/ClientSideTeleporter.java b/src/main/java/org/apache/sling/testing/teleporter/client/ClientSideTeleporter.java
index 758d503..2ed7330 100644
--- a/src/main/java/org/apache/sling/testing/teleporter/client/ClientSideTeleporter.java
+++ b/src/main/java/org/apache/sling/testing/teleporter/client/ClientSideTeleporter.java
@@ -48,14 +48,31 @@
     private String serverCredentials;
     private final Set<Class<?>> embeddedClasses = new HashSet<Class<?>>();
     
-    private InputStream buildTestBundle(Class<?> c, Collection<Class<?>> embeddedClasses, String bundleSymbolicName) {
+    private InputStream buildTestBundle(Class<?> c, Collection<Class<?>> embeddedClasses, String bundleSymbolicName) throws IOException {
         final TinyBundle b = TinyBundles.bundle()
             .set(Constants.BUNDLE_SYMBOLICNAME, bundleSymbolicName)
             .set("Sling-Test-Regexp", c.getName() + ".*")
             .add(c);
+        
+        // Embed specified classes
         for(Class<?> clz : embeddedClasses) {
             b.add(clz);
         }
+        
+        // Embed specified resources
+        if(!embeddedResourcePaths.isEmpty()) {
+            for(String path : embeddedResourcePaths) {
+                final ClassResourceVisitor.Processor p = new ClassResourceVisitor.Processor() {
+                    @Override
+                    public void process(String resourcePath, InputStream resourceStream) throws IOException {
+                        b.add(resourcePath, resourceStream);
+                    }
+                    
+                };
+                new ClassResourceVisitor(getClass(), path).visit(p);
+            }
+        }
+        
         return b.build(TinyBundles.withBnd());
     }
     
diff --git a/src/test/java/org/apache/sling/testing/teleporter/client/ClassResourceVisitorTest.java b/src/test/java/org/apache/sling/testing/teleporter/client/ClassResourceVisitorTest.java
new file mode 100644
index 0000000..af7b77f
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/teleporter/client/ClassResourceVisitorTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.testing.teleporter.client;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ClassResourceVisitorTest implements ClassResourceVisitor.Processor {
+
+    private Map<String, String> resources;
+    
+    @Override
+    public void process(String resourceName, InputStream resourceStream) throws IOException {
+        final StringWriter w = new StringWriter();
+        IOUtils.copy(resourceStream, w);
+        resources.put(resourceName, w.toString());
+    }
+
+    @Before
+    public void setup() {
+        resources = new HashMap<String, String>();
+    }
+    
+    private void assertResource(String name, String expected) {
+        final String content = resources.get(name);
+        assertNotNull("Expecting resource " + name + " (keys=" + resources.keySet() + ")", content);
+        assertTrue("Expecting " + name + " to contain " + expected, content.contains(expected));
+    }
+    
+    @Test
+    public void testSingleFile() throws IOException {
+        new ClassResourceVisitor(getClass(), "/somepath/two.txt").visit(this);
+        assertResource("/somepath/two.txt", "two");
+        assertEquals(1, resources.size());
+    }
+    
+    @Test
+    public void testSomepathFiles() throws IOException {
+        new ClassResourceVisitor(getClass(), "/somepath").visit(this);
+        assertResource("/somepath/two.txt", "two");
+        assertEquals(3, resources.size());
+    }
+    
+    @Test
+    public void testSubFiles() throws IOException {
+        new ClassResourceVisitor(getClass(), "/somepath/sub").visit(this);
+        assertResource("/somepath/sub/trois.txt", "three");
+        assertEquals(1, resources.size());
+    }
+    
+    @Test
+    public void testNotFound() throws Exception {
+        new ClassResourceVisitor(IOUtils.class, "/NOT_FOUND").visit(this);
+        assertEquals(0, resources.size());
+    }
+    
+    @Test
+    public void testJarResourcesFolder() throws Exception {
+        // Get resources from the IOUtils jar to test the jar protocol
+        new ClassResourceVisitor(IOUtils.class, "/META-INF/maven/commons-io/").visit(this);
+        assertResource("/META-INF/maven/commons-io/commons-io/pom.properties", "artifactId=commons-io");
+        assertResource("/META-INF/maven/commons-io/commons-io/pom.xml", "Licensed to the Apache Software Foundation");
+        assertEquals(2, resources.size());
+    }
+    
+    @Test
+    public void testSinglJarResource() throws Exception {
+        new ClassResourceVisitor(IOUtils.class, "/META-INF/maven/commons-io/commons-io/pom.properties").visit(this);
+        assertResource("/META-INF/maven/commons-io/commons-io/pom.properties", "artifactId=commons-io");
+        assertEquals(1, resources.size());
+    }
+}
\ No newline at end of file
diff --git a/src/test/resources/somepath/1.txt b/src/test/resources/somepath/1.txt
new file mode 100644
index 0000000..7abaf4e
--- /dev/null
+++ b/src/test/resources/somepath/1.txt
@@ -0,0 +1,19 @@
+one
+
+/*
+ * 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.
+ */
+
diff --git a/src/test/resources/somepath/sub/trois.txt b/src/test/resources/somepath/sub/trois.txt
new file mode 100644
index 0000000..4706298
--- /dev/null
+++ b/src/test/resources/somepath/sub/trois.txt
@@ -0,0 +1,18 @@
+three
+
+/*
+ * 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.
+ */
diff --git a/src/test/resources/somepath/two.txt b/src/test/resources/somepath/two.txt
new file mode 100644
index 0000000..73c2aa1
--- /dev/null
+++ b/src/test/resources/somepath/two.txt
@@ -0,0 +1,19 @@
+two
+
+/*
+ * 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.
+ */
+