This patch implements support for Bundle.findEntries() minus the support
for fragments, since they haven't been implemented yet. (FELIX-31)


git-svn-id: https://svn.apache.org/repos/asf/incubator/felix/trunk@423965 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/org.apache.felix.framework/src/main/java/org/apache/felix/framework/BundleImpl.java b/org.apache.felix.framework/src/main/java/org/apache/felix/framework/BundleImpl.java
index 0bf5448..8e83861 100644
--- a/org.apache.felix.framework/src/main/java/org/apache/felix/framework/BundleImpl.java
+++ b/org.apache.felix.framework/src/main/java/org/apache/felix/framework/BundleImpl.java
@@ -81,6 +81,11 @@
         return m_felix.getBundleEntryPaths(this, path);
     }
 
+    public Enumeration findEntries(String path, String filePattern, boolean recurse)
+    {
+        return m_felix.findBundleEntries(this, path, filePattern, recurse);
+    }
+
     public Dictionary getHeaders()
     {
         return m_felix.getBundleHeaders(this);
@@ -189,12 +194,6 @@
         return null;
     }
 
-    public Enumeration findEntries(String path, String filePattern, boolean recurse)
-    {
-        // TODO: Implement Bundle.findEntries()
-        return null;
-    }
-
     public boolean equals(Object obj)
     {
         if (obj instanceof BundleImpl)
diff --git a/org.apache.felix.framework/src/main/java/org/apache/felix/framework/Felix.java b/org.apache.felix.framework/src/main/java/org/apache/felix/framework/Felix.java
index 44539ba..7ab41a4 100644
--- a/org.apache.felix.framework/src/main/java/org/apache/felix/framework/Felix.java
+++ b/org.apache.felix.framework/src/main/java/org/apache/felix/framework/Felix.java
@@ -1068,6 +1068,38 @@
             .getContentLoader().getContent().getEntryPaths(path);
     }
 
+    /**
+     * Implementation for findEntries().
+    **/
+    public Enumeration findBundleEntries(
+        BundleImpl bundle, String path, String filePattern, boolean recurse)
+    {
+        // Strip leading '/' if present.
+        if ((path.length() > 0) && (path.charAt(0) == '/'))
+        {
+            path = path.substring(1);
+        }
+
+        // Sanity check the parameters.
+        if (path == null)
+        {
+            throw new IllegalArgumentException("The path for findEntries() cannot be null.");
+        }
+        filePattern = (filePattern == null) ? "*" : filePattern;
+
+
+        // Try to resolve the bundle per the spec.
+        resolveBundles(new Bundle[] { bundle });
+
+        // Get the entry enumeration from the module content.
+        Enumeration enumeration = bundle.getInfo().getCurrentModule()
+            .getContentLoader().getContent().findEntries(path, filePattern, recurse);
+
+        // Return a wrapper that will convert the entry strings to URLs.
+        return (enumeration == null)
+            ? null : new FindEntriesEnumeration(bundle, enumeration);
+    }
+
     protected ServiceReference[] getBundleRegisteredServices(BundleImpl bundle)
     {
         if (bundle.getInfo().getState() == Bundle.UNINSTALLED)
@@ -3746,6 +3778,41 @@
     }
 
     //
+    // Miscellaneous inner classes.
+    //
+
+    /**
+     * Used by findBundleEntries() method to wrap the enumeration
+     * returned by IContent.findEntries() to convert the returned
+     * Strings to URLs.
+     */
+    private static class FindEntriesEnumeration implements Enumeration
+    {
+        private BundleImpl m_bundle = null;
+        private Enumeration m_enumeration = null;
+
+        public FindEntriesEnumeration(BundleImpl bundle, Enumeration enumeration)
+        {
+            m_bundle = bundle;
+            m_enumeration = enumeration;
+        }
+        
+        public boolean hasMoreElements()
+        {
+            return m_enumeration.hasMoreElements();
+        }
+
+        public Object nextElement()
+        {
+            URL url = 
+                ((ContentLoaderImpl) m_bundle.getInfo().getCurrentModule()
+                    .getContentLoader()).getResourceFromContent(
+                        (String) m_enumeration.nextElement());
+            return url;
+        }
+    }
+
+    //
     // Locking related methods.
     //
 
diff --git a/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/ContentDirectoryContent.java b/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/ContentDirectoryContent.java
index f2f870e..267921d 100644
--- a/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/ContentDirectoryContent.java
+++ b/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/ContentDirectoryContent.java
@@ -127,6 +127,18 @@
         return new WrappedEnumeration(m_content.getEntryPaths(m_rootPath + path), m_rootPath);
     }
 
+    public Enumeration findEntries(String path, String filePattern, boolean recurse)
+    {
+        // This IContent method supports Bundle.findEntries(), which is used to
+        // browse the bundle JAR file, not the bundle's class path. However,
+        // this implementation of IContent is used purely to add support for
+        // directories in the bundle class path, thus it will never represent
+        // the actual bundle content, so this method should never be called.
+        // For now we will leave this unimplemented and if it becomes necessary
+        // it can be implemented later.
+        throw new UnsupportedOperationException("Not implemented, since it should not be used.");
+    }
+
     public String toString()
     {
         return "CONTENT DIR " + m_rootPath + " (" + m_content + ")";
diff --git a/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/DirectoryContent.java b/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/DirectoryContent.java
index 880cd7b..9868067 100644
--- a/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/DirectoryContent.java
+++ b/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/DirectoryContent.java
@@ -17,6 +17,7 @@
 package org.apache.felix.moduleloader;
 
 import java.io.*;
+import java.util.*;
 import java.util.Enumeration;
 import java.util.NoSuchElementException;
 
@@ -141,18 +142,31 @@
             path = path.substring(1);
         }
 
-        return new FileEnumeration(m_dir, path);
+        return new GetEntryPathsEnumeration(m_dir, path);
     }
 
+    public Enumeration findEntries(String path, String filePattern, boolean recurse)
+    {
+        if (!m_opened)
+        {
+            throw new IllegalStateException("JarContent is not open");
+        }
 
-    private static class FileEnumeration implements Enumeration
+        // Wrap entries enumeration to filter non-matching entries.
+        Enumeration e = new FindEntriesEnumeration(
+            m_dir, path, filePattern, recurse);
+        // Spec says to return null if there are no entries.
+        return (e.hasMoreElements()) ? e : null;
+    }
+
+    private static class GetEntryPathsEnumeration implements Enumeration
     {
         private File m_refDir = null;
         private File m_listDir = null;
         private File[] m_children = null;
         private int m_counter = 0;
 
-        public FileEnumeration(File refDir, String path)
+        public GetEntryPathsEnumeration(File refDir, String path)
         {
             m_refDir = refDir;
             m_listDir = new File(refDir, path);
@@ -186,4 +200,243 @@
             return sb.toString();
         }
     }
+
+    private static class FindEntriesEnumeration implements Enumeration
+    {
+        private File m_refDir = null;
+        private File m_listDir = null;
+        private String[] m_filePattern = null;
+        private boolean m_recurse = false;
+        private File[] m_children = null;
+        private int m_counter = 0;
+        private Object m_next = null;
+
+        public FindEntriesEnumeration(File refDir, String path, String filePattern, boolean recurse)
+        {
+            m_refDir = refDir;
+            m_listDir = new File(refDir, path);
+            m_filePattern = parseSubstring(filePattern);
+            m_recurse = recurse;
+            if (m_listDir.isDirectory())
+            {
+                if (m_recurse)
+                {
+                    m_children = listFilesRecursive(m_listDir);
+                }
+                else
+                {
+                    m_children = m_listDir.listFiles();
+                }
+            }
+            m_next = findNext();
+        }
+
+        public boolean hasMoreElements()
+        {
+            return (m_next != null);
+        }
+
+        public Object nextElement()
+        {
+            if (m_next == null)
+            {
+                throw new NoSuchElementException("No more entry paths.");
+            }
+            Object last = m_next;
+            m_next = findNext();
+            return last;
+        }
+
+        private Object findNext()
+        {
+            if ((m_children == null) || (m_counter >= m_children.length))
+            {
+                return null;
+            }
+
+            // NOTE: We assume here that directories are not returned,
+            // unlike getEntryPaths() above, where directories are returned;
+            // this may or may not be the correct spec interpretation.
+
+            // Ignore directories and file that do not match the file pattern.
+            while ((m_counter < m_children.length) &&
+                (m_children[m_counter].isDirectory() ||
+                !checkSubstring(m_filePattern, m_children[m_counter].getName())))
+            {
+                m_counter++;
+            }
+
+            // Return null if there is no more matches.
+            if (m_counter >= m_children.length)
+            {
+                return null;
+            }
+
+            // Remove the leading path of the reference directory, since the
+            // entry paths are supposed to be relative to the root.
+            StringBuffer sb = new StringBuffer(m_children[m_counter].getAbsolutePath());
+            sb.delete(0, m_refDir.getAbsolutePath().length() + 1);
+            m_counter++;
+
+            return sb.toString();
+        }
+
+        public File[] listFilesRecursive(File dir)
+        {
+            File[] children = dir.listFiles();
+            File[] combined = children;
+            for (int i = 0; i < children.length; i++)
+            {
+                if (children[i].isDirectory())
+                {
+                    File[] grandchildren = listFilesRecursive(children[i]);
+                    if (grandchildren.length > 0)
+                    {
+                        File[] tmp = new File[combined.length + grandchildren.length];
+                        System.arraycopy(combined, 0, tmp, 0, combined.length);
+                        System.arraycopy(grandchildren, 0, tmp, combined.length, grandchildren.length);
+                        combined = tmp;
+                    }
+                }
+            }
+            return combined;
+        }
+    }
+
+    //
+    // The following substring-related code was lifted and modified
+    // from the LDAP parser code.
+    //
+
+    private static String[] parseSubstring(String target)
+    {
+        List pieces = new ArrayList();
+        StringBuffer ss = new StringBuffer();
+        // int kind = SIMPLE; // assume until proven otherwise
+        boolean wasStar = false; // indicates last piece was a star
+        boolean leftstar = false; // track if the initial piece is a star
+        boolean rightstar = false; // track if the final piece is a star
+
+        int idx = 0;
+
+        // We assume (sub)strings can contain leading and trailing blanks
+loop:   for (;;)
+        {
+            if (idx >= target.length())
+            {
+                if (wasStar)
+                {
+                    // insert last piece as "" to handle trailing star
+                    rightstar = true;
+                }
+                else
+                {
+                    pieces.add(ss.toString());
+                    // accumulate the last piece
+                    // note that in the case of
+                    // (cn=); this might be
+                    // the string "" (!=null)
+                }
+                ss.setLength(0);
+                break loop;
+            }
+
+            char c = target.charAt(idx++);
+            if (c == '*')
+            {
+                if (wasStar)
+                {
+                    // encountered two successive stars;
+                    // I assume this is illegal
+                    throw new IllegalArgumentException("Invalid filter string: " + target);
+                }
+                if (ss.length() > 0)
+                {
+                    pieces.add(ss.toString()); // accumulate the pieces
+                    // between '*' occurrences
+                }
+                ss.setLength(0);
+                // if this is a leading star, then track it
+                if (pieces.size() == 0)
+                {
+                    leftstar = true;
+                }
+                ss.setLength(0);
+                wasStar = true;
+            }
+            else
+            {
+                wasStar = false;
+                ss.append(c);
+            }
+        }
+        if (leftstar || rightstar || pieces.size() > 1)
+        {
+            // insert leading and/or trailing "" to anchor ends
+            if (rightstar)
+            {
+                pieces.add("");
+            }
+            if (leftstar)
+            {
+                pieces.add(0, "");
+            }
+        }
+        return (String[]) pieces.toArray(new String[pieces.size()]);
+    }
+
+    private static boolean checkSubstring(String[] pieces, String s)
+    {
+        // Walk the pieces to match the string
+        // There are implicit stars between each piece,
+        // and the first and last pieces might be "" to anchor the match.
+        // assert (pieces.length > 1)
+        // minimal case is <string>*<string>
+
+        boolean result = false;
+        int len = pieces.length;
+
+loop:   for (int i = 0; i < len; i++)
+        {
+            String piece = (String) pieces[i];
+            int index = 0;
+            if (i == len - 1)
+            {
+                // this is the last piece
+                if (s.endsWith(piece))
+                {
+                    result = true;
+                }
+                else
+                {
+                    result = false;
+                }
+                break loop;
+            }
+            // initial non-star; assert index == 0
+            else if (i == 0)
+            {
+                if (!s.startsWith(piece))
+                {
+                    result = false;
+                    break loop;
+                }
+            }
+            // assert i > 0 && i < len-1
+            else
+            {
+                // Sure wish stringbuffer supported e.g. indexOf
+                index = s.indexOf(piece, index);
+                if (index < 0)
+                {
+                    result = false;
+                    break loop;
+                }
+            }
+            // start beyond the matching piece
+            index += piece.length();
+        }
+
+        return result;
+    }
 }
\ No newline at end of file
diff --git a/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/IContent.java b/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/IContent.java
index 7261fdc..c8a5392 100644
--- a/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/IContent.java
+++ b/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/IContent.java
@@ -29,4 +29,5 @@
     public InputStream getEntryAsStream(String name)
         throws IOException;
     public Enumeration getEntryPaths(String path);
+    public Enumeration findEntries(String path, String filePattern, boolean recurse);
 }
\ No newline at end of file
diff --git a/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/JarContent.java b/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/JarContent.java
index dc2d053..6f7dc0b 100644
--- a/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/JarContent.java
+++ b/org.apache.felix.framework/src/main/java/org/apache/felix/moduleloader/JarContent.java
@@ -17,6 +17,7 @@
 package org.apache.felix.moduleloader;
 
 import java.io.*;
+import java.util.*;
 import java.util.Enumeration;
 import java.util.NoSuchElementException;
 import java.util.zip.ZipEntry;
@@ -245,7 +246,36 @@
         }
 
         // Wrap entries enumeration to filter non-matching entries.
-        Enumeration e = new FilteredEnumeration(m_jarFile.entries(), path);
+        Enumeration e = new GetEntryPathsEnumeration(m_jarFile.entries(), path);
+        // Spec says to return null if there are no entries.
+        return (e.hasMoreElements()) ? e : null;
+    }
+
+    public synchronized Enumeration findEntries(
+        String path, String filePattern, boolean recurse)
+    {
+        if (!m_opened)
+        {
+            throw new IllegalStateException("JarContent is not open");
+        }
+
+        // Open JAR file if not already opened.
+        if (m_jarFile == null)
+        {
+            try
+            {
+                openJarFile();
+            }
+            catch (IOException ex)
+            {
+                System.err.println("JarContent: " + ex);
+                return null;
+            }
+        }
+
+        // Wrap entries enumeration to filter non-matching entries.
+        Enumeration e = new FindEntriesEnumeration(
+            m_jarFile.entries(), path, filePattern, recurse);
         // Spec says to return null if there are no entries.
         return (e.hasMoreElements()) ? e : null;
     }
@@ -263,13 +293,13 @@
         return "JAR " + m_file.getPath();
     }
 
-    private static class FilteredEnumeration implements Enumeration
+    private static class GetEntryPathsEnumeration implements Enumeration
     {
         private Enumeration m_enumeration = null;
         private String m_path = null;
         private Object m_next = null;
 
-        public FilteredEnumeration(Enumeration enumeration, String path)
+        public GetEntryPathsEnumeration(Enumeration enumeration, String path)
         {
             m_enumeration = enumeration;
             // Add a '/' to the end if not present.
@@ -304,15 +334,16 @@
             {
                 // Get the next zip entry.
                 ZipEntry entry = (ZipEntry) m_enumeration.nextElement();
-                // Check to see if it is a child of the specified path.
+                // Check to see if it is a descendent of the specified path.
                 if (!entry.getName().equals(m_path) && entry.getName().startsWith(m_path))
                 {
                     // Verify that it is a child of the path and not a
                     // grandchild by examining its remaining path length.
-                    // this code uses the knowledge that zip entries
+                    // This code uses the knowledge that zip entries
                     // corresponding to directories end in '/'. It checks
                     // to see if the next occurrence of '/' is also the
-                    // end of the string or if there are no more occurrences.
+                    // end of the string, which means that this entry
+                    // represents a child directory of the path.
                     int idx = entry.getName().indexOf('/', m_path.length());
                     if ((idx < 0) || (idx == (entry.getName().length() - 1)))
                     {
@@ -323,4 +354,225 @@
             return null;
         }
     }
+
+    private static class FindEntriesEnumeration implements Enumeration
+    {
+        private Enumeration m_enumeration = null;
+        private String m_path = null;
+        private String[] m_filePattern = null;
+        private boolean m_recurse = false;
+        private Object m_next = null;
+
+        public FindEntriesEnumeration(
+            Enumeration enumeration, String path, String filePattern, boolean recurse)
+        {
+            m_enumeration = enumeration;
+            // Add a '/' to the end if not present.
+            m_path = (path.length() > 0) && (path.charAt(path.length() - 1) != '/')
+                ? path + "/" : path;
+            m_filePattern = parseSubstring(filePattern);
+            m_recurse = recurse;
+            m_next = findNext();
+        }
+
+        public boolean hasMoreElements()
+        {
+            return (m_next != null);
+        }
+
+        public Object nextElement()
+        {
+            if (m_next == null)
+            {
+                throw new NoSuchElementException("No more entry paths.");
+            }
+            Object last = m_next;
+            m_next = findNext();
+            return last;
+        }
+
+        private Object findNext()
+        {
+            // This method filters the entries of the zip file, such that
+            // it only displays the contents of the directory specified by
+            // the path argument either recursively or not; much like using
+            // "ls -R" or "ls" to list the contents of a directory, respectively.
+            while (m_enumeration.hasMoreElements())
+            {
+                // Get the next zip entry.
+                ZipEntry entry = (ZipEntry) m_enumeration.nextElement();
+                String entryName = entry.getName();
+                // Check to see if it is a descendent of the specified path.
+                if (!entryName.equals(m_path) && entryName.startsWith(m_path))
+                {
+                    // NOTE: We assume here that directories are not returned,
+                    // unlike getEntryPaths() above, where directories are returned;
+                    // this may or may not be the correct spec interpretation.
+
+                    // If this is recursive, then simply verify that the
+                    // entry is not a directory my making sure it does not
+                    // end with '/'. If this is not recursive, then verify
+                    // that the entry is a child of the path and not a
+                    // grandchild by examining its remaining path length.
+                    // This code uses the knowledge that zip entries
+                    // corresponding to directories end in '/'.
+                    int idx = entryName.indexOf('/', m_path.length());
+                    if ((m_recurse && (entryName.charAt(entryName.length() - 1) != '/'))
+                        || (idx < 0))
+                    {
+                        // Get the last element of the path.
+                        idx = entryName.lastIndexOf('/');
+                        String lastElement = entryName;
+                        if (idx >= 0)
+                        {
+                            lastElement = entryName.substring(idx + 1);
+                        }
+                        // See if the file pattern matches the last element of the path.
+                        if (checkSubstring(m_filePattern, lastElement))
+                        {
+                            return entry.getName();
+                        }
+                    }
+                }
+            }
+            return null;
+        }
+    }
+
+    //
+    // The following substring-related code was lifted and modified
+    // from the LDAP parser code.
+    //
+
+    private static String[] parseSubstring(String target)
+    {
+        List pieces = new ArrayList();
+        StringBuffer ss = new StringBuffer();
+        // int kind = SIMPLE; // assume until proven otherwise
+        boolean wasStar = false; // indicates last piece was a star
+        boolean leftstar = false; // track if the initial piece is a star
+        boolean rightstar = false; // track if the final piece is a star
+
+        int idx = 0;
+
+        // We assume (sub)strings can contain leading and trailing blanks
+loop:   for (;;)
+        {
+            if (idx >= target.length())
+            {
+                if (wasStar)
+                {
+                    // insert last piece as "" to handle trailing star
+                    rightstar = true;
+                }
+                else
+                {
+                    pieces.add(ss.toString());
+                    // accumulate the last piece
+                    // note that in the case of
+                    // (cn=); this might be
+                    // the string "" (!=null)
+                }
+                ss.setLength(0);
+                break loop;
+            }
+
+            char c = target.charAt(idx++);
+            if (c == '*')
+            {
+                if (wasStar)
+                {
+                    // encountered two successive stars;
+                    // I assume this is illegal
+                    throw new IllegalArgumentException("Invalid filter string: " + target);
+                }
+                if (ss.length() > 0)
+                {
+                    pieces.add(ss.toString()); // accumulate the pieces
+                    // between '*' occurrences
+                }
+                ss.setLength(0);
+                // if this is a leading star, then track it
+                if (pieces.size() == 0)
+                {
+                    leftstar = true;
+                }
+                ss.setLength(0);
+                wasStar = true;
+            }
+            else
+            {
+                wasStar = false;
+                ss.append(c);
+            }
+        }
+        if (leftstar || rightstar || pieces.size() > 1)
+        {
+            // insert leading and/or trailing "" to anchor ends
+            if (rightstar)
+            {
+                pieces.add("");
+            }
+            if (leftstar)
+            {
+                pieces.add(0, "");
+            }
+        }
+        return (String[]) pieces.toArray(new String[pieces.size()]);
+    }
+
+    private static boolean checkSubstring(String[] pieces, String s)
+    {
+        // Walk the pieces to match the string
+        // There are implicit stars between each piece,
+        // and the first and last pieces might be "" to anchor the match.
+        // assert (pieces.length > 1)
+        // minimal case is <string>*<string>
+
+        boolean result = false;
+        int len = pieces.length;
+
+loop:   for (int i = 0; i < len; i++)
+        {
+            String piece = (String) pieces[i];
+            int index = 0;
+            if (i == len - 1)
+            {
+                // this is the last piece
+                if (s.endsWith(piece))
+                {
+                    result = true;
+                }
+                else
+                {
+                    result = false;
+                }
+                break loop;
+            }
+            // initial non-star; assert index == 0
+            else if (i == 0)
+            {
+                if (!s.startsWith(piece))
+                {
+                    result = false;
+                    break loop;
+                }
+            }
+            // assert i > 0 && i < len-1
+            else
+            {
+                // Sure wish stringbuffer supported e.g. indexOf
+                index = s.indexOf(piece, index);
+                if (index < 0)
+                {
+                    result = false;
+                    break loop;
+                }
+            }
+            // start beyond the matching piece
+            index += piece.length();
+        }
+
+        return result;
+    }
 }
\ No newline at end of file