001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *  http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    
020    package org.apache.xbean.osgi.bundle.util;
021    
022    import java.io.IOException;
023    import java.io.InputStream;
024    import java.net.URL;
025    import java.util.ArrayList;
026    import java.util.Collection;
027    import java.util.Enumeration;
028    import java.util.HashMap;
029    import java.util.HashSet;
030    import java.util.LinkedHashSet;
031    import java.util.List;
032    import java.util.Map;
033    import java.util.Set;
034    import java.util.zip.ZipEntry;
035    import java.util.zip.ZipInputStream;
036    
037    import org.apache.xbean.osgi.bundle.util.BundleDescription.ExportPackage;
038    import org.apache.xbean.osgi.bundle.util.BundleDescription.HeaderEntry;
039    import org.apache.xbean.osgi.bundle.util.BundleDescription.RequireBundle;
040    import org.osgi.framework.Bundle;
041    import org.osgi.service.packageadmin.ExportedPackage;
042    import org.osgi.service.packageadmin.PackageAdmin;
043    import org.osgi.service.packageadmin.RequiredBundle;
044    import org.slf4j.Logger;
045    import org.slf4j.LoggerFactory;
046    
047    /**
048     * Finds all available classes to a bundle by scanning Bundle-ClassPath,
049     * Import-Package, and Require-Bundle headers of the given bundle and its fragments.
050     * DynamicImport-Package header is not considered during scanning.
051     *
052     * @version $Rev: 942661 $ $Date: 2010-05-10 07:17:20 +0200 (Mon, 10 May 2010) $
053     */
054    public class BundleClassFinder {
055    
056        private static final Logger logger = LoggerFactory.getLogger(BundleClassFinder.class);
057    
058        public static final ClassDiscoveryFilter FULL_CLASS_DISCOVERY_FILTER = new DummyDiscoveryFilter();
059    
060        public static final ClassDiscoveryFilter IMPORTED_PACKAGE_EXCLUSIVE_FILTER = new NonImportedPackageDiscoveryFilter();
061    
062        protected static final String EXT = ".class";
063    
064        protected static final String PATTERN = "*.class";
065    
066        protected Bundle bundle;
067    
068        protected PackageAdmin packageAdmin;
069    
070        private Map<Bundle, Set<String>> classMap;
071    
072        protected ClassDiscoveryFilter discoveryFilter;
073    
074        public BundleClassFinder(PackageAdmin packageAdmin, Bundle bundle) {
075            this(packageAdmin, bundle, FULL_CLASS_DISCOVERY_FILTER);
076        }
077    
078        public BundleClassFinder(PackageAdmin packageAdmin, Bundle bundle, ClassDiscoveryFilter discoveryFilter) {
079            this.packageAdmin = packageAdmin;
080            this.bundle = bundle;
081            this.discoveryFilter = discoveryFilter;
082        }
083    
084        public List<Class> loadClasses(Set<String> classes) {
085            List<Class> loadedClasses = new ArrayList<Class>(classes.size());
086            for (String clazz : classes) {
087                try {
088                    loadedClasses.add(bundle.loadClass(clazz));
089                } catch (Exception ignore) {
090                    // ignore
091                }
092            }
093            return loadedClasses;
094        }
095    
096        /**
097         * Finds all available classes to the bundle. Some of the classes in the returned set
098         * might not be loadable.
099         *
100         * @return classes visible to the bundle. Not all classes returned might be loadable.
101         */
102        public Set<String> find() {
103            Set<String> classes = new LinkedHashSet<String>();
104            classMap = new HashMap<Bundle, Set<String>>();
105            if (discoveryFilter.rangeDiscoveryRequired(DiscoveryRange.IMPORT_PACKAGES)) {
106                scanImportPackages(classes, bundle, bundle);
107            }
108            if (discoveryFilter.rangeDiscoveryRequired(DiscoveryRange.REQUIRED_BUNDLES)) {
109                scanRequireBundles(classes, bundle);
110            }
111            if (discoveryFilter.rangeDiscoveryRequired(DiscoveryRange.BUNDLE_CLASSPATH)) {
112                scanBundleClassPath(classes, bundle);
113            }
114            if (discoveryFilter.rangeDiscoveryRequired(DiscoveryRange.FRAGMENT_BUNDLES)) {
115                Bundle[] fragments = packageAdmin.getFragments(bundle);
116                if (fragments != null) {
117                    for (Bundle fragment : fragments) {
118                        scanImportPackages(classes, bundle, fragment);
119                        scanRequireBundles(classes, fragment);
120                        scanBundleClassPath(classes, fragment);
121                    }
122                }
123            }
124            classMap.clear();
125            return classes;
126        }
127    
128        protected boolean isClassAcceptable(String name, InputStream in) throws IOException {
129            return true;
130        }
131    
132        protected boolean isClassAcceptable(URL url) {
133            return true;
134        }
135    
136        protected BundleClassFinder createSubBundleClassFinder(PackageAdmin packageAdmin, Bundle bundle, ClassDiscoveryFilter classDiscoveryFilter) {
137            return new BundleClassFinder(packageAdmin, bundle, classDiscoveryFilter);
138        }
139    
140        protected String toJavaStyleClassName(String name) {
141            if (name.endsWith(EXT)) {
142                name = name.substring(0, name.length() - EXT.length());
143            }
144            name = name.replace('/', '.');
145            return name;
146        }
147    
148        /**
149         * Get the normal Java style package name from the parameter className.
150         * If the className is ended with .class extension, e.g.  /org/apache/geronimo/TestCass.class or org.apache.geronimo.TestClass.class,
151         *      then org/apache/geronimo is returned
152         * If the className is not ended with .class extension, e.g.  /org/apache/geronimo/TestCass or org.apache.geronimo.TestClass,
153         *      then org/apache/geronimo is returned
154         * @return Normal Java style package name, should be like org.apache.geronimo
155         */
156        protected String toJavaStylePackageName(String className) {
157            if (className.endsWith(EXT)) {
158                className = className.substring(0, className.length() - EXT.length());
159            }
160            className = className.replace('/', '.');
161            int iLastDotIndex = className.lastIndexOf('.');
162            if (iLastDotIndex != -1) {
163                return className.substring(0, iLastDotIndex);
164            } else {
165                return "";
166            }
167        }
168    
169        private Set<String> findAllClasses(Bundle bundle, ClassDiscoveryFilter userClassDiscoveryFilter, Set<String> exportedPackageNames) {
170            Set<String> allClasses = classMap.get(bundle);
171            if (allClasses == null) {
172                BundleClassFinder finder = createSubBundleClassFinder(packageAdmin, bundle, new ImportExclusivePackageDiscoveryFilterAdapter(userClassDiscoveryFilter, exportedPackageNames));
173                allClasses = finder.find();
174                classMap.put(bundle, allClasses);
175            }
176            return allClasses;
177        }
178    
179        private Set<String> findAllClasses(Bundle bundle, String packageName) {
180            Set<String> allClasses = classMap.get(bundle);
181            if (allClasses == null) {
182                BundleClassFinder finder = createSubBundleClassFinder(packageAdmin, bundle, new ImportExclusivePackageDiscoveryFilter(packageName));
183                allClasses = finder.find();
184                classMap.put(bundle, allClasses);
185            }
186            return allClasses;
187        }
188    
189        private void scanImportPackages(Collection<String> classes, Bundle host, Bundle fragment) {
190            BundleDescription description = new BundleDescription(fragment.getHeaders());
191            List<BundleDescription.ImportPackage> imports = description.getExternalImports();
192            for (BundleDescription.ImportPackage packageImport : imports) {
193                String packageName = packageImport.getName();
194                if (discoveryFilter.packageDiscoveryRequired(packageName)) {
195                    ExportedPackage[] exports = packageAdmin.getExportedPackages(packageName);
196                    Bundle wiredBundle = isWired(host, exports);
197                    if (wiredBundle != null) {
198                        Set<String> allClasses = findAllClasses(wiredBundle, packageName);
199                        classes.addAll(allClasses);
200                    }
201                }
202            }
203        }
204    
205        private void scanRequireBundles(Collection<String> classes, Bundle bundle) {
206            BundleDescription description = new BundleDescription(bundle.getHeaders());
207            List<RequireBundle> requiredBundleList = description.getRequireBundle();
208            for (RequireBundle requiredBundle : requiredBundleList) {
209                RequiredBundle[] requiredBundles = packageAdmin.getRequiredBundles(requiredBundle.getName());
210                Bundle wiredBundle = isWired(bundle, requiredBundles);
211                if (wiredBundle != null) {
212                    BundleDescription wiredBundleDescription = new BundleDescription(wiredBundle.getHeaders());
213                    List<ExportPackage> exportPackages = wiredBundleDescription.getExportPackage();
214                    Set<String> exportedPackageNames = new HashSet<String>();
215                    for (ExportPackage exportPackage : exportPackages) {
216                        exportedPackageNames.add(exportPackage.getName());
217                    }
218                    Set<String> allClasses = findAllClasses(wiredBundle, discoveryFilter, exportedPackageNames);
219                    classes.addAll(allClasses);
220                }
221            }
222        }
223    
224        private void scanBundleClassPath(Collection<String> resources, Bundle bundle) {
225            BundleDescription description = new BundleDescription(bundle.getHeaders());
226            List<HeaderEntry> paths = description.getBundleClassPath();
227            if (paths.isEmpty()) {
228                scanDirectory(resources, bundle, "/");
229            } else {
230                for (HeaderEntry path : paths) {
231                    String name = path.getName();
232                    if (name.equals(".") || name.equals("/")) {
233                        // scan root
234                        scanDirectory(resources, bundle, "/");
235                    } else if (name.endsWith(".jar") || name.endsWith(".zip")) {
236                        // scan embedded jar/zip
237                        scanZip(resources, bundle, name);
238                    } else {
239                        // assume it's a directory
240                        scanDirectory(resources, bundle, "/" + name);
241                    }
242                }
243            }
244        }
245    
246        private void scanDirectory(Collection<String> classes, Bundle bundle, String basePath) {
247            basePath = addSlash(basePath);
248            if (!discoveryFilter.directoryDiscoveryRequired(basePath)) {
249                return;
250            }
251            Enumeration<URL> e = bundle.findEntries(basePath, PATTERN, true);
252            if (e != null) {
253                while (e.hasMoreElements()) {
254                    URL u = e.nextElement();
255                    String entryName = u.getPath().substring(basePath.length());
256                    if (discoveryFilter.packageDiscoveryRequired(toJavaStylePackageName(entryName))) {
257                        if (isClassAcceptable(u)) {
258                            classes.add(toJavaStyleClassName(entryName));
259                        }
260                    }
261                }
262            }
263        }
264    
265        private void scanZip(Collection<String> classes, Bundle bundle, String zipName) {
266            if (!discoveryFilter.jarFileDiscoveryRequired(zipName)) {
267                return;
268            }
269            URL zipEntry = bundle.getEntry(zipName);
270            if (zipEntry == null) {
271                return;
272            }
273            ZipInputStream in = null;
274            try {
275                in = new ZipInputStream(zipEntry.openStream());
276                ZipEntry entry;
277                while ((entry = in.getNextEntry()) != null) {
278                    String name = entry.getName();
279                    if (name.endsWith(EXT) && discoveryFilter.packageDiscoveryRequired(toJavaStylePackageName(name))) {
280                        if (isClassAcceptable(name, in)) {
281                            classes.add(toJavaStyleClassName(name));
282                        }
283                    }
284                }
285            } catch (IOException ignore) {
286                logger.warn("Fail to check zip file " + zipName, ignore);
287            } finally {
288                if (in != null) {
289                    try {
290                        in.close();
291                    } catch (IOException e) {
292                    }
293                }
294            }
295        }
296    
297        protected String addSlash(String name) {
298            if (!name.endsWith("/")) {
299                name = name + "/";
300            }
301            return name;
302        }
303    
304        protected Bundle isWired(Bundle bundle, ExportedPackage[] exports) {
305            if (exports != null) {
306                for (ExportedPackage exportedPackage : exports) {
307                    Bundle[] importingBundles = exportedPackage.getImportingBundles();
308                    if (importingBundles != null) {
309                        for (Bundle importingBundle : importingBundles) {
310                            if (importingBundle == bundle) {
311                                return exportedPackage.getExportingBundle();
312                            }
313                        }
314                    }
315                }
316            }
317            return null;
318        }
319    
320        protected Bundle isWired(Bundle bundle, RequiredBundle[] requiredBundles) {
321            if (requiredBundles != null) {
322                for (RequiredBundle requiredBundle : requiredBundles) {
323                    Bundle[] requiringBundles = requiredBundle.getRequiringBundles();
324                    if (requiringBundles != null) {
325                        for (Bundle requiringBundle : requiringBundles) {
326                            if (requiringBundle == bundle) {
327                                return requiredBundle.getBundle();
328                            }
329                        }
330                    }
331                }
332            }
333            return null;
334        }
335    
336        public static class DummyDiscoveryFilter implements ClassDiscoveryFilter {
337    
338    
339            public boolean directoryDiscoveryRequired(String url) {
340                return true;
341            }
342    
343    
344            public boolean rangeDiscoveryRequired(DiscoveryRange discoveryRange) {
345                return true;
346            }
347    
348    
349            public boolean jarFileDiscoveryRequired(String url) {
350                return true;
351            }
352    
353    
354            public boolean packageDiscoveryRequired(String packageName) {
355                return true;
356            }
357        }
358    
359        public static class NonImportedPackageDiscoveryFilter implements ClassDiscoveryFilter {
360    
361    
362            public boolean directoryDiscoveryRequired(String url) {
363                return true;
364            }
365    
366    
367            public boolean jarFileDiscoveryRequired(String url) {
368                return true;
369            }
370    
371    
372            public boolean packageDiscoveryRequired(String packageName) {
373                return true;
374            }
375    
376    
377            public boolean rangeDiscoveryRequired(DiscoveryRange discoveryRange) {
378                return !discoveryRange.equals(DiscoveryRange.IMPORT_PACKAGES);
379            }
380        }
381    
382        private static class ImportExclusivePackageDiscoveryFilter implements ClassDiscoveryFilter {
383    
384            private String expectedPckageName;
385    
386            public ImportExclusivePackageDiscoveryFilter(String expectedPckageName) {
387                this.expectedPckageName = expectedPckageName;
388            }
389    
390    
391            public boolean directoryDiscoveryRequired(String url) {
392                return true;
393            }
394    
395    
396            public boolean jarFileDiscoveryRequired(String url) {
397                return true;
398            }
399    
400    
401            public boolean packageDiscoveryRequired(String packageName) {
402                return expectedPckageName.equals(packageName);
403            }
404    
405    
406            public boolean rangeDiscoveryRequired(DiscoveryRange discoveryRange) {
407                return !discoveryRange.equals(DiscoveryRange.IMPORT_PACKAGES);
408            }
409        }
410    
411        private static class ImportExclusivePackageDiscoveryFilterAdapter implements ClassDiscoveryFilter {
412    
413            private Set<String> acceptedPackageNames;
414    
415            private ClassDiscoveryFilter classDiscoveryFilter;
416    
417            public ImportExclusivePackageDiscoveryFilterAdapter(ClassDiscoveryFilter classDiscoveryFilter, Set<String> acceptedPackageNames) {
418                this.classDiscoveryFilter = classDiscoveryFilter;
419                this.acceptedPackageNames = acceptedPackageNames;
420            }
421    
422    
423            public boolean directoryDiscoveryRequired(String url) {
424                return true;
425            }
426    
427    
428            public boolean jarFileDiscoveryRequired(String url) {
429                return true;
430            }
431    
432    
433            public boolean packageDiscoveryRequired(String packageName) {
434                return acceptedPackageNames.contains(packageName) && classDiscoveryFilter.packageDiscoveryRequired(packageName);
435            }
436    
437    
438            public boolean rangeDiscoveryRequired(DiscoveryRange discoveryRange) {
439                return !discoveryRange.equals(DiscoveryRange.IMPORT_PACKAGES);
440            }
441        }
442    }