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
020package org.apache.xbean.osgi.bundle.util;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.net.URL;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.Enumeration;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.LinkedHashSet;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034import java.util.zip.ZipEntry;
035import java.util.zip.ZipInputStream;
036
037import org.apache.xbean.osgi.bundle.util.BundleDescription.ExportPackage;
038import org.apache.xbean.osgi.bundle.util.BundleDescription.HeaderEntry;
039import org.apache.xbean.osgi.bundle.util.BundleDescription.RequireBundle;
040import org.osgi.framework.Bundle;
041import org.osgi.service.packageadmin.ExportedPackage;
042import org.osgi.service.packageadmin.PackageAdmin;
043import org.osgi.service.packageadmin.RequiredBundle;
044import org.slf4j.Logger;
045import 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 (lun. 10 mai 2010) $
053 */
054public 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}