View Javadoc

1   /*
2    * Copyright 2006-2008 the original author or authors.
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
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  	// currently not used but might be in the future
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  		// NoClassDefFoundError
90  		if (loadingThrowable instanceof NoClassDefFoundError) {
91  			className = loadingThrowable.getMessage();
92  			if (className != null)
93  				className = className.replace('/', '.');
94  		}
95  		// ClassNotFound
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 				// Check out all the classes.
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 		// Reject global packages
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 					// Everything looks ok, but is the root bundle importing the
166 					// dependent class also?
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 								// TODO -- this is very rough, check the bundle
173 								// classpath also.
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 		// log.info("Examining Bundle [" + bundle.getBundleId() + ": " + bname +
200 		// "]");
201 		// Check for version matching
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 		// Do more detailed checks
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 		// Found the resource, check that it is exported.
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 					// Ignored
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 				// Copy entries from the real jar to our virtual jar
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 						// ignore it
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 		// Check imports
309 		String imports = (String) dict.get(Constants.IMPORT_PACKAGE);
310 		Version v = getVersion(imports, packageName);
311 		if (v != null) {
312 			return v;
313 		}
314 		// Check for dynamic imports
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 				// check for version only if we have a match
365 				if (pkg.equals(packageName)) {
366 					// no version determined, find one
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 							// found it
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 									// remove brackets
382 									value = value.substring(2, value.length() - 2);
383 									int commaIndex = value.indexOf(COMMA);
384 
385 									// TODO: currently, only the left side is considered
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 								// check quotes
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 		// spit the statement into packages but consider "
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 }