FELIX-6294: add a slash to urls for directories
diff --git a/framework/src/main/java/org/apache/felix/framework/BundleRevisionImpl.java b/framework/src/main/java/org/apache/felix/framework/BundleRevisionImpl.java
index 5aa9daa..388e426 100644
--- a/framework/src/main/java/org/apache/felix/framework/BundleRevisionImpl.java
+++ b/framework/src/main/java/org/apache/felix/framework/BundleRevisionImpl.java
@@ -514,6 +514,10 @@
         {
             if (contentPath.get(i).hasEntry(name))
             {
+                if (!name.endsWith("/") && contentPath.get(i).isDirectory(name))
+                {
+                    name += "/";
+                }
                 url = createURL(i + 1, name);
             }
         }
@@ -552,6 +556,10 @@
             {
                 if (contentPath.get(i).hasEntry(name))
                 {
+                    if (!name.endsWith("/") && contentPath.get(i).isDirectory(name))
+                    {
+                        name += "/";
+                    }
                     // Use the class path index + 1 for creating the path so
                     // that we can differentiate between module content URLs
                     // (where the path will start with 0) and module class
@@ -588,6 +596,10 @@
             // Check the module content.
             if (getContent().hasEntry(name))
             {
+                if (!name.endsWith("/") && getContent().isDirectory(name))
+                {
+                    name += "/";
+                }
                 // Module content URLs start with 0, whereas module
                 // class path URLs start with the index into the class
                 // path + 1.
diff --git a/framework/src/main/java/org/apache/felix/framework/ExtensionManager.java b/framework/src/main/java/org/apache/felix/framework/ExtensionManager.java
index 5ed9955..5f0ee34 100644
--- a/framework/src/main/java/org/apache/felix/framework/ExtensionManager.java
+++ b/framework/src/main/java/org/apache/felix/framework/ExtensionManager.java
@@ -869,7 +869,14 @@
         };
     }
 
-    public boolean hasEntry(String name) {
+    public boolean hasEntry(String name)
+    {
+        return false;
+    }
+
+    @Override
+    public boolean isDirectory(String name)
+    {
         return false;
     }
 
diff --git a/framework/src/main/java/org/apache/felix/framework/cache/ConnectContentContent.java b/framework/src/main/java/org/apache/felix/framework/cache/ConnectContentContent.java
index 88e6dec..a37c598 100644
--- a/framework/src/main/java/org/apache/felix/framework/cache/ConnectContentContent.java
+++ b/framework/src/main/java/org/apache/felix/framework/cache/ConnectContentContent.java
@@ -68,6 +68,12 @@
     }
 
     @Override
+    public boolean isDirectory(String name)
+    {
+        return m_content.getEntry(name).map(entry -> entry.getName().endsWith("/")).orElse(false);
+    }
+
+    @Override
     public Enumeration<String> getEntries()
     {
         try
diff --git a/framework/src/main/java/org/apache/felix/framework/cache/Content.java b/framework/src/main/java/org/apache/felix/framework/cache/Content.java
index 79580c9..4ec2ecd 100644
--- a/framework/src/main/java/org/apache/felix/framework/cache/Content.java
+++ b/framework/src/main/java/org/apache/felix/framework/cache/Content.java
@@ -49,6 +49,18 @@
 
     /**
      * <p>
+     * This method determines if the specified named entry is contained in
+     * the associated content and is a directory. The entry name is a relative path with '/'
+     * separators.
+     * </p>
+     * @param name The name of the entry to find.
+     * @return <tt>true</tt> if a corresponding entry was found and is a directory, <tt>false</tt>
+     *         otherwise.
+     **/
+    boolean isDirectory(String name);
+
+    /**
+     * <p>
      * Returns an enumeration of entry names as <tt>String</tt> objects.
      * An entry name is a path constructed with '/' as path element
      * separators and is relative to the root of the content. Entry names
diff --git a/framework/src/main/java/org/apache/felix/framework/cache/ContentDirectoryContent.java b/framework/src/main/java/org/apache/felix/framework/cache/ContentDirectoryContent.java
index e1f04ac..1673270 100644
--- a/framework/src/main/java/org/apache/felix/framework/cache/ContentDirectoryContent.java
+++ b/framework/src/main/java/org/apache/felix/framework/cache/ContentDirectoryContent.java
@@ -52,6 +52,14 @@
         return m_content.hasEntry(m_rootPath + name);
     }
 
+    @Override
+    public boolean isDirectory(String name)
+    {
+        name = getName(name);
+
+        return m_content.isDirectory(m_rootPath + name);
+    }
+
     public Enumeration<String> getEntries()
     {
         Enumeration<String> result = new EntriesEnumeration(m_content.getEntries(), m_rootPath);
diff --git a/framework/src/main/java/org/apache/felix/framework/cache/DirectoryContent.java b/framework/src/main/java/org/apache/felix/framework/cache/DirectoryContent.java
index 0240933..c462b55 100644
--- a/framework/src/main/java/org/apache/felix/framework/cache/DirectoryContent.java
+++ b/framework/src/main/java/org/apache/felix/framework/cache/DirectoryContent.java
@@ -83,6 +83,18 @@
                 ? BundleCache.getSecureAction().isFileDirectory(file) : true);
     }
 
+    @Override
+    public boolean isDirectory(String name)
+    {
+        name = getName(name);
+
+        // Return true if the file associated with the entry exists,
+        // unless the entry name ends with "/", in which case only
+        // return true if the file is really a directory.
+        File file = new File(m_dir, name);
+        return BundleCache.getSecureAction().isFileDirectory(file);
+    }
+
     public Enumeration<String> getEntries()
     {
         // Wrap entries enumeration to filter non-matching entries.
diff --git a/framework/src/main/java/org/apache/felix/framework/cache/JarContent.java b/framework/src/main/java/org/apache/felix/framework/cache/JarContent.java
index 79c2249..fd1dda0 100644
--- a/framework/src/main/java/org/apache/felix/framework/cache/JarContent.java
+++ b/framework/src/main/java/org/apache/felix/framework/cache/JarContent.java
@@ -101,7 +101,7 @@
         }
     }
 
-    public boolean hasEntry(String name) throws IllegalStateException
+    public boolean hasEntry(String name)
     {
         try
         {
@@ -114,6 +114,20 @@
         }
     }
 
+    @Override
+    public boolean isDirectory(String name)
+    {
+        try
+        {
+            ZipEntry ze = m_zipFile.getEntry(name);
+            return ze != null && ze.isDirectory();
+        }
+        catch (Exception ex)
+        {
+            return false;
+        }
+    }
+
     public Enumeration<String> getEntries()
     {
         // Wrap entries enumeration to filter non-matching entries.
diff --git a/framework/src/main/java/org/apache/felix/framework/util/MultiReleaseContent.java b/framework/src/main/java/org/apache/felix/framework/util/MultiReleaseContent.java
index 29a5e30..fa1b48d 100644
--- a/framework/src/main/java/org/apache/felix/framework/util/MultiReleaseContent.java
+++ b/framework/src/main/java/org/apache/felix/framework/util/MultiReleaseContent.java
@@ -88,6 +88,12 @@
     }
 
     @Override
+    public boolean isDirectory(String name)
+    {
+        return m_content.isDirectory(findPath(name));
+    }
+
+    @Override
     public Enumeration<String> getEntries()
     {
         Enumeration<String> entries = m_content.getEntries();
diff --git a/framework/src/test/java/org/apache/felix/framework/ResourceLoadingTest.java b/framework/src/test/java/org/apache/felix/framework/ResourceLoadingTest.java
index a89e430..19d6d07 100644
--- a/framework/src/test/java/org/apache/felix/framework/ResourceLoadingTest.java
+++ b/framework/src/test/java/org/apache/felix/framework/ResourceLoadingTest.java
@@ -79,7 +79,8 @@
         cacheDir = null;
     }
 
-    public void testResourceLoadingWithHash() throws Exception {
+    public void testResourceLoadingWithHash() throws Exception
+    {
         String bmf = "Bundle-SymbolicName: cap.bundle\n"
             + "Bundle-Version: 1.2.3.Blah\n"
             + "Bundle-ManifestVersion: 2\n"
@@ -132,6 +133,47 @@
         }
     }
 
+    public void testResourceLoadingWithDirectory() throws Exception
+    {
+        String bmf = "Bundle-SymbolicName: cap.bundle\n"
+                + "Bundle-Version: 1.2.3.Blah\n"
+                + "Bundle-ManifestVersion: 2\n"
+                + "Import-Package: org.osgi.framework\n";
+        File bundleFile = File.createTempFile("felix-bundle", ".jar", tempDir);
+
+        Manifest mf = new Manifest(new ByteArrayInputStream(bmf.getBytes("utf-8")));
+        mf.getMainAttributes().putValue("Manifest-Version", "1.0");
+        JarOutputStream os = new JarOutputStream(new FileOutputStream(bundleFile), mf);
+
+        String name = "bla/bli/blub";
+        os.putNextEntry(new ZipEntry("bla/"));
+        os.putNextEntry(new ZipEntry("bla/bli/"));
+        os.putNextEntry(new ZipEntry(name));
+        os.write("This is a Test".getBytes());
+        os.close();
+
+        Bundle testBundle = felix.getBundleContext().installBundle(bundleFile.toURI().toASCIIString());
+
+        testBundle.start();
+
+        assertEquals(Bundle.ACTIVE, testBundle.getState());
+        assertTrue(testBundle.getResource("bla").toExternalForm().endsWith("/"));
+        assertTrue(testBundle.getEntry("bla").toExternalForm().endsWith("/"));
+        assertTrue(testBundle.adapt(BundleWiring.class).getClassLoader().getResource("bla").toExternalForm().endsWith("/"));
+        assertTrue(testBundle.getResource("bla/").toExternalForm().endsWith("/"));
+        assertTrue(testBundle.getEntry("bla/").toExternalForm().endsWith("/"));
+        assertTrue(testBundle.adapt(BundleWiring.class).getClassLoader().getResource("bla/").toExternalForm().endsWith("/"));
+        assertTrue(testBundle.getResource("bla/bli").toExternalForm().endsWith("/"));
+        assertTrue(testBundle.getEntry("bla/bli").toExternalForm().endsWith("/"));
+        assertTrue(testBundle.adapt(BundleWiring.class).getClassLoader().getResource("bla/bli").toExternalForm().endsWith("/"));
+        assertTrue(testBundle.getResource("bla/bli/").toExternalForm().endsWith("/"));
+        assertTrue(testBundle.getEntry("bla/bli/").toExternalForm().endsWith("/"));
+        assertTrue(testBundle.adapt(BundleWiring.class).getClassLoader().getResource("bla/bli/").toExternalForm().endsWith("/"));
+        assertTrue(testBundle.getResource("bla/bli/blub").toExternalForm().endsWith("/blub"));
+        assertTrue(testBundle.getEntry("bla/bli/blub").toExternalForm().endsWith("/blub"));
+        assertTrue(testBundle.adapt(BundleWiring.class).getClassLoader().getResource("bla/bli/blub").toExternalForm().endsWith("/blub"));
+    }
+
     private static void deleteDir(File root) throws IOException
     {
         if (root.isDirectory())