1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.springframework.osgi.util;
18
19 import java.io.IOException;
20 import java.net.URL;
21 import java.util.ArrayList;
22 import java.util.Dictionary;
23 import java.util.Enumeration;
24 import java.util.List;
25 import java.util.StringTokenizer;
26 import java.util.jar.JarEntry;
27 import java.util.jar.JarInputStream;
28 import java.util.regex.Pattern;
29
30 import org.apache.commons.logging.Log;
31 import org.osgi.framework.Bundle;
32 import org.osgi.framework.BundleContext;
33 import org.osgi.framework.Constants;
34 import org.osgi.framework.Version;
35 import org.springframework.util.ClassUtils;
36 import org.springframework.util.ObjectUtils;
37
38 /**
39 * Utility class used for debugging exceptions in OSGi environment, such as
40 * class loading errors.
41 *
42 * The main entry point is
43 * {@link #debugClassLoadingThrowable(Throwable, Bundle, Class[])} which will
44 * try to determine the cause by trying to load the given interfaces using the
45 * given bundle.
46 *
47 * <p/> The debugging process can be potentially expensive.
48 *
49 * @author Costin Leau
50 * @author Andy Piper
51 */
52 public abstract class DebugUtils {
53
54 private static final String EQUALS = "=";
55 private static final String DOUBLE_QUOTE = "\"";
56 private static final String SEMI_COLON = ";";
57 private static final String COMMA = ",";
58 /** use degradable logger */
59 private static final Log log = LogUtils.createLogger(DebugUtils.class);
60
61
62 private static final String PACKAGE_REGEX = "([^;,]+(?:;?\\w+:?=((\"[^\"]+\")|([^,]+)))*)+";
63 private static final Pattern PACKAGE_PATTERN = Pattern.compile(PACKAGE_REGEX);
64
65
66 /**
67 * Tries to debug the cause of the {@link Throwable}s that can appear when
68 * loading classes in OSGi environments (for example when creating proxies).
69 *
70 * <p/> This method will try to determine the class that caused the problem
71 * and to search for it in the given bundle or through the classloaders of
72 * the given classes.
73 *
74 * It will look at the classes are visible by the given bundle on debug
75 * level and do a bundle discovery process on trace level.
76 *
77 * The method accepts also an array of classes which will be used for
78 * loading the 'problematic' class that caused the exception on debug level.
79 *
80 * @param loadingThrowable class loading {@link Throwable} (such as
81 * {@link NoClassDefFoundError} or {@link ClassNotFoundException})
82 * @param bundle bundle used for loading the classes
83 * @param classes (optional) array of classes that will be used for loading
84 * the problematic class
85 */
86 public static void debugClassLoadingThrowable(Throwable loadingThrowable, Bundle bundle, Class[] classes) {
87
88 String className = null;
89
90 if (loadingThrowable instanceof NoClassDefFoundError) {
91 className = loadingThrowable.getMessage();
92 if (className != null)
93 className = className.replace('/', '.');
94 }
95
96 else if (loadingThrowable instanceof ClassNotFoundException) {
97 className = loadingThrowable.getMessage();
98
99 if (className != null)
100 className = className.replace('/', '.');
101 }
102
103 if (className != null) {
104
105 debugClassLoading(bundle, className, null);
106
107 if (!ObjectUtils.isEmpty(classes) && log.isDebugEnabled()) {
108 StringBuffer message = new StringBuffer();
109
110
111 for (int i = 0; i < classes.length; i++) {
112 ClassLoader cl = classes[i].getClassLoader();
113 String cansee = "cannot";
114 if (ClassUtils.isPresent(className, cl))
115 cansee = "can";
116 message.append(classes[i] + " is loaded by " + cl + " which " + cansee + " see " + className);
117 }
118 log.debug(message);
119 }
120 }
121 }
122
123 /**
124 * Tries (through a best-guess attempt) to figure out why a given class
125 * could not be found. This method will search the given bundle and its
126 * classpath to determine the reason for which the class cannot be loaded.
127 *
128 * <p/> This method tries to be effective especially when the dealing with
129 * {@link NoClassDefFoundError} caused by failure of loading transitive
130 * classes (such as getting a NCDFE when loading <code>foo.A</code>
131 * because <code>bar.B</code> cannot be found).
132 *
133 * @param bundle the bundle to search for (and which should do the loading)
134 * @param className the name of the class that failed to be loaded in dot
135 * format (i.e. java.lang.Thread)
136 * @param rootClassName the name of the class that triggered the loading
137 * (i.e. java.lang.Runnable)
138 */
139 public static void debugClassLoading(Bundle bundle, String className, String rootClassName) {
140 boolean trace = log.isTraceEnabled();
141 if (!trace)
142 return;
143
144 Dictionary dict = bundle.getHeaders();
145 String bname = dict.get(Constants.BUNDLE_NAME) + "(" + dict.get(Constants.BUNDLE_SYMBOLICNAME) + ")";
146 if (trace)
147 log.trace("Could not find class [" + className + "] required by [" + bname + "] scanning available bundles");
148
149 BundleContext context = OsgiBundleUtils.getBundleContext(bundle);
150 String packageName = className.substring(0, className.lastIndexOf('.'));
151
152 if (className.indexOf('.') < 0) {
153 if (trace)
154 log.trace("Class is not in a package, its unlikely that this will work");
155 return;
156 }
157 Version iversion = hasImport(bundle, packageName);
158 if (iversion != null && context != null) {
159 if (trace)
160 log.trace("Class is correctly imported as version [" + iversion + "], checking providing bundles");
161 Bundle[] bundles = context.getBundles();
162 for (int i = 0; i < bundles.length; i++) {
163 if (bundles[i].getBundleId() != bundle.getBundleId()) {
164 Version exported = checkBundleForClass(bundles[i], className, iversion);
165
166
167 if (exported != null && exported.equals(iversion) && rootClassName != null) {
168 for (int j = 0; j < bundles.length; j++) {
169 Version rootexport = hasExport(bundles[j], rootClassName.substring(0,
170 rootClassName.lastIndexOf('.')));
171 if (rootexport != null) {
172
173
174 Version rootimport = hasImport(bundles[j], packageName);
175 if (rootimport == null || !rootimport.equals(iversion)) {
176 if (trace)
177 log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundles[j])
178 + "] exports [" + rootClassName + "] as version [" + rootexport
179 + "] but does not import dependent package [" + packageName
180 + "] at version [" + iversion + "]");
181 }
182 }
183 }
184 }
185 }
186 }
187 }
188 if (hasExport(bundle, packageName) != null) {
189 if (trace)
190 log.trace("Class is exported, checking this bundle");
191 checkBundleForClass(bundle, className, iversion);
192 }
193 }
194
195 private static Version checkBundleForClass(Bundle bundle, String name, Version iversion) {
196 String packageName = name.substring(0, name.lastIndexOf('.'));
197 Version hasExport = hasExport(bundle, packageName);
198
199
200
201
202 if (hasExport != null && !hasExport.equals(iversion)) {
203 log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] exports [" + packageName
204 + "] as version [" + hasExport + "] but version [" + iversion + "] was required");
205 return hasExport;
206 }
207
208 String cname = name.substring(packageName.length() + 1) + ".class";
209 Enumeration e = bundle.findEntries("/" + packageName.replace('.', '/'), cname, false);
210 if (e == null) {
211 if (hasExport != null) {
212 URL url = checkBundleJarsForClass(bundle, name);
213 if (url != null) {
214 log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] contains [" + cname
215 + "] in embedded jar [" + url.toString() + "] but exports the package");
216 }
217 else {
218 log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] does not contain ["
219 + cname + "] but exports the package");
220 }
221 }
222
223 String root = "/";
224 String fileName = packageName;
225 if (packageName.lastIndexOf(".") >= 0) {
226 root = root + packageName.substring(0, packageName.lastIndexOf(".")).replace('.', '/');
227 fileName = packageName.substring(packageName.lastIndexOf(".") + 1).replace('.', '/');
228 }
229 Enumeration pe = bundle.findEntries(root, fileName, false);
230 if (pe != null) {
231 if (hasExport != null) {
232 log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] contains package ["
233 + packageName + "] and exports it");
234 }
235 else {
236 log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] contains package ["
237 + packageName + "] but does not export it");
238 }
239
240 }
241 }
242
243 else {
244 if (hasExport != null) {
245 log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] contains resource [" + cname
246 + "] and it is correctly exported as version [" + hasExport + "]");
247 Class c = null;
248 try {
249 c = bundle.loadClass(name);
250 }
251 catch (ClassNotFoundException e1) {
252
253 }
254 log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] loadClass [" + cname
255 + "] returns [" + c + "]");
256 }
257 else {
258 log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] contains resource [" + cname
259 + "] but its package is not exported");
260 }
261 }
262 return hasExport;
263 }
264
265 private static URL checkBundleJarsForClass(Bundle bundle, String name) {
266 String cname = name.replace('.', '/') + ".class";
267 for (Enumeration e = bundle.findEntries("/", "*.jar", true); e != null && e.hasMoreElements();) {
268 URL url = (URL) e.nextElement();
269 JarInputStream jin = null;
270 try {
271 jin = new JarInputStream(url.openStream());
272
273 for (JarEntry ze = jin.getNextJarEntry(); ze != null; ze = jin.getNextJarEntry()) {
274 if (ze.getName().equals(cname)) {
275 jin.close();
276 return url;
277 }
278 }
279 }
280 catch (IOException e1) {
281 log.trace("Skipped " + url.toString() + ": " + e1.getMessage());
282 }
283
284 finally {
285 if (jin != null) {
286 try {
287 jin.close();
288 }
289 catch (Exception ex) {
290
291 }
292 }
293 }
294
295 }
296 return null;
297 }
298
299 /**
300 * Get the version of a package import from a bundle.
301 *
302 * @param bundle
303 * @param packageName
304 * @return
305 */
306 private static Version hasImport(Bundle bundle, String packageName) {
307 Dictionary dict = bundle.getHeaders();
308
309 String imports = (String) dict.get(Constants.IMPORT_PACKAGE);
310 Version v = getVersion(imports, packageName);
311 if (v != null) {
312 return v;
313 }
314
315 String dynimports = (String) dict.get(Constants.DYNAMICIMPORT_PACKAGE);
316 if (dynimports != null) {
317 for (StringTokenizer strok = new StringTokenizer(dynimports, COMMA); strok.hasMoreTokens();) {
318 StringTokenizer parts = new StringTokenizer(strok.nextToken(), SEMI_COLON);
319 String pkg = parts.nextToken().trim();
320 if (pkg.endsWith(".*") && packageName.startsWith(pkg.substring(0, pkg.length() - 2)) || pkg.equals("*")) {
321 Version version = Version.emptyVersion;
322 for (; parts.hasMoreTokens();) {
323 String modifier = parts.nextToken().trim();
324 if (modifier.startsWith("version")) {
325 version = Version.parseVersion(modifier.substring(modifier.indexOf(EQUALS) + 1).trim());
326 }
327 }
328 return version;
329 }
330 }
331 }
332 return null;
333 }
334
335 private static Version hasExport(Bundle bundle, String packageName) {
336 Dictionary dict = bundle.getHeaders();
337 return getVersion((String) dict.get(Constants.EXPORT_PACKAGE), packageName);
338 }
339
340 /**
341 * Get the version of a package name.
342 *
343 * @param stmt
344 * @param packageName
345 * @return
346 */
347 private static Version getVersion(String stmt, String packageName) {
348 if (stmt != null) {
349 String[] pkgs = splitIntoPackages(stmt);
350
351 for (int packageIndex = 0; packageIndex < pkgs.length; packageIndex++) {
352 String pkgToken = pkgs[packageIndex].trim();
353 String pkg = null;
354 Version version = null;
355 int firstDirectiveIndex = pkgToken.indexOf(SEMI_COLON);
356 if (firstDirectiveIndex > -1) {
357 pkg = pkgToken.substring(0, firstDirectiveIndex);
358 }
359 else {
360 pkg = pkgToken;
361 version = Version.emptyVersion;
362 }
363
364
365 if (pkg.equals(packageName)) {
366
367 if (version == null) {
368 String[] directiveTokens = pkgToken.substring(firstDirectiveIndex + 1).split(SEMI_COLON);
369 for (int directiveTokenIndex = 0; directiveTokenIndex < directiveTokens.length; directiveTokenIndex++) {
370 String directive = directiveTokens[directiveTokenIndex].trim();
371
372 if (directive.startsWith(Constants.VERSION_ATTRIBUTE)) {
373 String value = directive.substring(directive.indexOf(EQUALS) + 1).trim();
374
375 boolean lowEqualTo = value.startsWith("\"[");
376 boolean lowGreaterThen = value.startsWith("\"(");
377 if (lowEqualTo || lowGreaterThen) {
378 boolean highEqualTo = value.endsWith("]\"");
379 boolean highLessThen = value.endsWith(")\"");
380
381
382 value = value.substring(2, value.length() - 2);
383 int commaIndex = value.indexOf(COMMA);
384
385
386 Version left = Version.parseVersion(value.substring(0, commaIndex));
387 Version right = Version.parseVersion(value.substring(commaIndex + 1));
388
389 return left;
390 }
391
392
393 if (value.startsWith("\"")) {
394 return Version.parseVersion(value.substring(1, value.length() - 1));
395 }
396 return Version.parseVersion(value);
397 }
398 }
399 if (version == null) {
400 version = Version.emptyVersion;
401 }
402 }
403 return version;
404 }
405 }
406 }
407 return null;
408 }
409
410 private static String[] splitIntoPackages(String stmt) {
411
412 List pkgs = new ArrayList(2);
413
414 StringBuffer pkg = new StringBuffer();
415 boolean ignoreComma = false;
416 for (int stringIndex = 0; stringIndex < stmt.length(); stringIndex++) {
417 char currentChar = stmt.charAt(stringIndex);
418 if (currentChar == ',') {
419 if (ignoreComma) {
420 pkg.append(currentChar);
421 }
422 else {
423 pkgs.add(pkg.toString());
424 pkg = new StringBuffer();
425 ignoreComma = false;
426 }
427 }
428 else {
429 if (currentChar == '\"') {
430 ignoreComma = !ignoreComma;
431 }
432 pkg.append(currentChar);
433 }
434 }
435 pkgs.add(pkg.toString());
436 return (String[]) pkgs.toArray(new String[pkgs.size()]);
437 }
438 }