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: 1160131 $ $Date: 2011-08-22 15:07:20 +0800 (Mon, 22 Aug 2011) $
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 = BundleUtils.unwrapBundle(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 }