Implement BundleWiring.listResources(). (FELIX-2950)


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1152864 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java b/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java
index 767b776..024e571 100644
--- a/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java
+++ b/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java
@@ -33,11 +33,13 @@
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
+import java.util.TreeSet;
 import org.apache.felix.framework.cache.JarContent;
 import org.apache.felix.framework.cache.Content;
 import org.apache.felix.framework.capabilityset.SimpleFilter;
@@ -66,6 +68,8 @@
 
 public class BundleWiringImpl implements BundleWiring
 {
+    public final static int LISTRESOURCES_DEBUG = 1048576;
+
     public final static int EAGER_ACTIVATION = 0;
     public final static int LAZY_ACTIVATION = 1;
 
@@ -634,15 +638,317 @@
         return null;
     }
 
-    public Collection<String> listResources(String path, String filePattern, int options)
+// TODO: OSGi R4.3 - Should this be synchronized or should we take a snapshot?
+    // Thread local to detect class loading cycles.
+    private final ThreadLocal m_listResourcesCycleCheck = new ThreadLocal();
+
+    public synchronized Collection<String> listResources(
+        String path, String filePattern, int options)
+    {
+        // Implementation note: If you enable the DEBUG option for
+        // listResources() to print from where each resource comes,
+        // it will not give 100% accurate answers in the face of
+        // Require-Bundle cycles with overlapping content since
+        // the actual source will depend on who does the class load
+        // first. Further, normal class loaders cache class load
+        // results so it is always the same subsequently, but we
+        // don't do that here so it will always return a different
+        // result depending upon who is asking. Moral to the story:
+        // don't do cycles and certainly don't do them with
+        // overlapping content.
+
+        Collection<String> resources = null;
+
+        // Normalize path.
+        if ((path.length() > 0) && (path.charAt(0) == '/'))
+        {
+            path = path.substring(1);
+        }
+        if ((path.length() > 0) && (path.charAt(path.length() - 1) != '/'))
+        {
+            path = path + '/';
+        }
+
+        // Parse the file filter.
+        filePattern = (filePattern == null) ? "*" : filePattern;
+        List<String> pattern = SimpleFilter.parseSubstring(filePattern);
+
+        // We build an internal collection of ResourceSources, since this
+        // allows us to print out additional debug information.
+        Collection<ResourceSource> sources = listResourcesInternal(path, pattern, options);
+        if (sources != null)
+        {
+            boolean debug = (options & LISTRESOURCES_DEBUG) > 0;
+            resources = new TreeSet<String>();
+            for (ResourceSource source : sources)
+            {
+                if (debug)
+                {
+                    resources.add(source.toString());
+                }
+                else
+                {
+                    resources.add(source.m_resource);
+                }
+            }
+        }
+        return resources;
+    }
+
+    private Collection<ResourceSource> listResourcesInternal(
+        String path, List<String> pattern, int options)
     {
         if (isInUse())
         {
-            throw new UnsupportedOperationException("Not supported yet.");
+            boolean recurse = (options & BundleWiring.LISTRESOURCES_RECURSE) > 0;
+            boolean localOnly = (options & BundleWiring.LISTRESOURCES_LOCAL) > 0;
+
+            // Check for cycles, which can happen with Require-Bundle.
+            Set<String> cycles = (Set<String>) m_listResourcesCycleCheck.get();
+            if (cycles == null)
+            {
+                cycles = new HashSet<String>();
+                m_listResourcesCycleCheck.set(cycles);
+            }
+            if (cycles.contains(path))
+            {
+                return Collections.EMPTY_LIST;
+            }
+            cycles.add(path);
+
+            try
+            {
+                // Calculate set of remote resources (i.e., those either
+                // imported or required).
+                Collection<ResourceSource> remoteResources = new TreeSet<ResourceSource>();
+                // Imported packages cannot have merged content, so we need to
+                // keep track of these packages.
+                Set<String> noMerging = new HashSet<String>();
+                // Loop through wires to compute remote resources.
+                for (BundleWire bw : m_wires)
+                {
+                    if (bw.getCapability().getNamespace()
+                        .equals(BundleRevision.PACKAGE_NAMESPACE))
+                    {
+                        // For imported packages, we only need to calculate
+                        // the remote resources of the specific imported package.
+                        remoteResources.addAll(
+                            calculateRemotePackageResources(
+                                bw, bw.getCapability(), recurse,
+                                    path, pattern, noMerging));
+                    }
+                    else if (bw.getCapability().getNamespace()
+                        .equals(BundleRevision.BUNDLE_NAMESPACE))
+                    {
+                        // For required bundles, all declared package capabilities
+                        // from the required bundle will be available to requirers,
+                        // so get the target required bundle's declared packages
+                        // and handle them in a similar fashion to a normal import
+                        // except that their content can be merged with local
+                        // packages.
+                        List<BundleCapability> exports =
+                            bw.getProviderWiring().getRevision()
+                                .getDeclaredCapabilities(BundleRevision.PACKAGE_NAMESPACE);
+                        for (BundleCapability export : exports)
+                        {
+                            remoteResources.addAll(
+                                calculateRemotePackageResources(
+                                    bw, export, recurse, path, pattern, null));
+                        }
+
+                        // Since required bundle may reexport bundles it requires,
+                        // check its wires for this case.
+                        List<BundleWire> requiredBundles =
+                            bw.getProviderWiring().getRequiredWires(
+                                BundleRevision.BUNDLE_NAMESPACE);
+                        for (BundleWire rbWire : requiredBundles)
+                        {
+                            String visibility =
+                                rbWire.getRequirement().getDirectives()
+                                    .get(Constants.VISIBILITY_DIRECTIVE);
+                            if ((visibility != null)
+                                && (visibility.equals(Constants.VISIBILITY_REEXPORT)))
+                            {
+                                // For each reexported required bundle, treat them
+                                // in a similar fashion as a normal required bundle
+                                // by including all of their declared package
+                                // capabilities in the requiring bundle's class
+                                // space.
+                                List<BundleCapability> reexports =
+                                    rbWire.getProviderWiring().getRevision()
+                                        .getDeclaredCapabilities(BundleRevision.PACKAGE_NAMESPACE);
+                                for (BundleCapability reexport : reexports)
+                                {
+                                    remoteResources.addAll(
+                                        calculateRemotePackageResources(
+                                            bw, reexport, recurse, path, pattern, null));
+                                }
+                            }
+                        }
+                    }
+                }
+
+                // Calculate set of local resources (i.e., those contained
+                // in the revision or its fragments).
+                Collection<ResourceSource> localResources = new TreeSet<ResourceSource>();
+                // Get the revision's content path, which includes contents
+                // from fragments.
+                List<Content> contentPath = m_revision.getContentPath();
+                for (Content content : contentPath)
+                {
+                    Enumeration<String> e = content.getEntries();
+                    if (e != null)
+                    {
+                        while (e.hasMoreElements())
+                        {
+                            String resource = e.nextElement();
+                            String resourcePath = getTrailingPath(resource);
+                            if (!noMerging.contains(resourcePath))
+                            {
+                                if ((!recurse && resourcePath.equals(path))
+                                    || (recurse && resourcePath.startsWith(path)))
+                                {
+                                    if (matchesPattern(pattern, getPathHead(resource)))
+                                    {
+                                        localResources.add(
+                                            new ResourceSource(resource, m_revision));
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+
+                if (localOnly)
+                {
+                    return localResources;
+                }
+                else
+                {
+                    remoteResources.addAll(localResources);
+                    return remoteResources;
+                }
+            }
+            finally
+            {
+                cycles.remove(path);
+                if (cycles.isEmpty())
+                {
+                    m_listResourcesCycleCheck.set(null);
+                }
+            }
         }
         return null;
     }
 
+    private Collection<ResourceSource> calculateRemotePackageResources(
+        BundleWire bw, BundleCapability cap, boolean recurse,
+        String path, List<String> pattern, Set<String> noMerging)
+    {
+        Collection<ResourceSource> resources = Collections.EMPTY_SET;
+
+        // Convert package name to a path.
+        String subpath = (String) cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE);
+        subpath = subpath.replace('.', '/') + '/';
+        // If necessary, record that this package should not be merged
+        // with local content.
+        if (noMerging != null)
+        {
+            noMerging.add(subpath);
+        }
+
+        // If we are not recuring, check for path equality or if
+        // we are recursing, check that the subpath starts with
+        // the target path.
+        if ((!recurse && subpath.equals(path))
+            || (recurse && subpath.startsWith(path)))
+        {
+            // Delegate to the original provider wiring to have it calculate
+            // the list of resources in the package. In this case, we don't
+            // want to recurse since we want the precise package.
+            resources =
+                ((BundleWiringImpl) bw.getProviderWiring()).listResourcesInternal(
+                    subpath, pattern, 0);
+
+            // The delegatedResources result will include subpackages
+            // which need to be filtered out, since imported packages
+            // do not give access to subpackages. If a subpackage is
+            // imported, it will be added by its own wire.
+            for (Iterator<ResourceSource> it = resources.iterator();
+                it.hasNext(); )
+            {
+                ResourceSource reqResource = it.next();
+                if (reqResource.m_resource.charAt(
+                    reqResource.m_resource.length() - 1) == '/')
+                {
+                    it.remove();
+                }
+            }
+        }
+        // If we are not recursing, but the required package
+        // is a child of the desired path, then include its
+        // immediate child package. We do this so that it is
+        // possible to use listResources() to walk the resource
+        // tree similar to doing a directory walk one level
+        // at a time.
+        else if (!recurse && subpath.startsWith(path))
+        {
+            int idx = subpath.indexOf('/', path.length());
+            if (idx >= 0)
+            {
+                subpath = subpath.substring(0, idx + 1);
+            }
+            if (matchesPattern(pattern, getPathHead(subpath)))
+            {
+                resources = Collections.singleton(
+                    new ResourceSource(subpath, bw.getProviderWiring().getRevision()));
+            }
+        }
+
+        return resources;
+    }
+
+    private static String getPathHead(String resource)
+    {
+        if (resource.length() == 0)
+        {
+            return resource;
+        }
+        int idx = (resource.charAt(resource.length() - 1) == '/')
+            ? resource.lastIndexOf('/', resource.length() - 2)
+            : resource.lastIndexOf('/');
+        if (idx < 0)
+        {
+            return resource;
+        }
+        return resource.substring(idx + 1);
+    }
+
+    private static String getTrailingPath(String resource)
+    {
+        if (resource.length() == 0)
+        {
+            return null;
+        }
+        int idx = (resource.charAt(resource.length() - 1) == '/')
+            ? resource.lastIndexOf('/', resource.length() - 2)
+            : resource.lastIndexOf('/');
+        if (idx < 0)
+        {
+            return "";
+        }
+        return resource.substring(0, idx + 1);
+    }
+
+    private static boolean matchesPattern(List<String> pattern, String resource)
+    {
+        if (resource.charAt(resource.length() - 1) == '/')
+        {
+            resource = resource.substring(0, resource.length() - 1);
+        }
+        return SimpleFilter.compareSubstring(pattern, resource);
+    }
+
     public Bundle getBundle()
     {
         return m_revision.getBundle();
@@ -2083,6 +2389,48 @@
         return url;
     }
 
+    private static class ResourceSource implements Comparable<ResourceSource>
+    {
+        public final String m_resource;
+        public final BundleRevision m_revision;
+
+        public ResourceSource(String resource, BundleRevision revision)
+        {
+            m_resource = resource;
+            m_revision = revision;
+        }
+
+        @Override
+        public boolean equals(Object o)
+        {
+            if (o instanceof ResourceSource)
+            {
+                return m_resource.equals(((ResourceSource) o).m_resource);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode()
+        {
+            return m_resource.hashCode();
+        }
+
+        public int compareTo(ResourceSource t)
+        {
+            return m_resource.compareTo(t.m_resource);
+        }
+
+        @Override
+        public String toString()
+        {
+            return m_resource
+                + " -> "
+                + m_revision.getSymbolicName()
+                + " [" + m_revision + "]";
+        }
+    }
+
     private static String diagnoseClassLoadError(
         StatefulResolver resolver, BundleRevision revision, String name)
     {
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 b1cb0e4..09ace44 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
@@ -57,7 +57,7 @@
      * </p>
      * @returns An enumeration of entry names or <tt>null</tt>.
     **/
-    Enumeration getEntries();
+    Enumeration<String> getEntries();
 
     /**
      * <p>
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 4d8a2dd..892ddf2 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
@@ -1,4 +1,4 @@
-/* 
+/*
  * 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
@@ -55,7 +55,7 @@
         return m_content.hasEntry(m_rootPath + name);
     }
 
-    public Enumeration getEntries()
+    public Enumeration<String> getEntries()
     {
         return 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 4e7e929..8e2af9c 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
@@ -71,10 +71,10 @@
                 ? BundleCache.getSecureAction().isFileDirectory(file) : true);
     }
 
-    public Enumeration getEntries()
+    public Enumeration<String> getEntries()
     {
         // Wrap entries enumeration to filter non-matching entries.
-        Enumeration e = new EntriesEnumeration(m_dir);
+        Enumeration<String> e = new EntriesEnumeration(m_dir);
 
         // Spec says to return null if there are no entries.
         return (e.hasMoreElements()) ? e : null;
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 12ca117..f8d0bd5 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,10 +101,10 @@
         }
     }
 
-    public Enumeration getEntries()
+    public Enumeration<String> getEntries()
     {
         // Wrap entries enumeration to filter non-matching entries.
-        Enumeration e = new EntriesEnumeration(m_zipFile.entries());
+        Enumeration<String> e = new EntriesEnumeration(m_zipFile.entries());
 
         // Spec says to return null if there are no entries.
         return (e.hasMoreElements()) ? e : null;
@@ -490,7 +490,7 @@
         }
     }
 
-    private static class EntriesEnumeration implements Enumeration
+    private static class EntriesEnumeration implements Enumeration<String>
     {
         private final Enumeration m_enumeration;
 
@@ -504,17 +504,17 @@
             return m_enumeration.hasMoreElements();
         }
 
-        public Object nextElement()
+        public String nextElement()
         {
             return ((ZipEntry) m_enumeration.nextElement()).getName();
         }
     }
 
-    private static class DevNullRunnable implements Runnable 
+    private static class DevNullRunnable implements Runnable
     {
         private final InputStream m_in;
 
-        public DevNullRunnable(InputStream in) 
+        public DevNullRunnable(InputStream in)
         {
             m_in = in;
         }
@@ -527,12 +527,12 @@
                 {
                     while (m_in.read() != -1){}
                 }
-                finally 
+                finally
                 {
                     m_in.close();
                 }
             }
-            catch (Exception ex) 
+            catch (Exception ex)
             {
                 // Not much we can do - maybe we should log it?
             }