/****************************************************************************** * Copyright (c) 2006, 2010 VMware Inc., Oracle Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html and the Apache License v2.0 * is available at http://www.opensource.org/licenses/apache2.0.php. * You may elect to redistribute this code under either of these licenses. * * Contributors: * VMware Inc. * Oracle Inc. *****************************************************************************/ package org.eclipse.gemini.blueprint.util; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Dictionary; import java.util.Enumeration; import java.util.List; import java.util.StringTokenizer; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.regex.Pattern; import org.apache.commons.logging.Log; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.Version; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; /** * Utility class used for debugging exceptions in OSGi environment, such as * class loading errors. * * The main entry point is * {@link #debugClassLoadingThrowable(Throwable, Bundle, Class[])} which will * try to determine the cause by trying to load the given interfaces using the * given bundle. * * <p/> The debugging process can be potentially expensive. * * @author Costin Leau * @author Andy Piper */ public abstract class DebugUtils { private static final String EQUALS = "="; private static final String DOUBLE_QUOTE = "\""; private static final String SEMI_COLON = ";"; private static final String COMMA = ","; /** use degradable logger */ private static final Log log = LogUtils.createLogger(DebugUtils.class); // currently not used but might be in the future private static final String PACKAGE_REGEX = "([^;,]+(?:;?\\w+:?=((\"[^\"]+\")|([^,]+)))*)+"; private static final Pattern PACKAGE_PATTERN = Pattern.compile(PACKAGE_REGEX); /** * Tries to debug the cause of the {@link Throwable}s that can appear when * loading classes in OSGi environments (for example when creating proxies). * * <p/> This method will try to determine the class that caused the problem * and to search for it in the given bundle or through the classloaders of * the given classes. * * It will look at the classes are visible by the given bundle on debug * level and do a bundle discovery process on trace level. * * The method accepts also an array of classes which will be used for * loading the 'problematic' class that caused the exception on debug level. * * @param loadingThrowable class loading {@link Throwable} (such as * {@link NoClassDefFoundError} or {@link ClassNotFoundException}) * @param bundle bundle used for loading the classes * @param classes (optional) array of classes that will be used for loading * the problematic class */ public static void debugClassLoadingThrowable(Throwable loadingThrowable, Bundle bundle, Class<?>[] classes) { String className = null; // NoClassDefFoundError if (loadingThrowable instanceof NoClassDefFoundError) { className = loadingThrowable.getMessage(); if (className != null) className = className.replace('/', '.'); } // ClassNotFound else if (loadingThrowable instanceof ClassNotFoundException) { className = loadingThrowable.getMessage(); if (className != null) className = className.replace('/', '.'); } if (className != null) { debugClassLoading(bundle, className, null); if (!ObjectUtils.isEmpty(classes) && log.isDebugEnabled()) { StringBuilder message = new StringBuilder(); // Check out all the classes. for (int i = 0; i < classes.length; i++) { ClassLoader cl = classes[i].getClassLoader(); String cansee = "cannot"; if (ClassUtils.isPresent(className, cl)) cansee = "can"; message.append(classes[i] + " is loaded by " + cl + " which " + cansee + " see " + className); } log.debug(message); } } } /** * Tries (through a best-guess attempt) to figure out why a given class * could not be found. This method will search the given bundle and its * classpath to determine the reason for which the class cannot be loaded. * * <p/> This method tries to be effective especially when the dealing with * {@link NoClassDefFoundError} caused by failure of loading transitive * classes (such as getting a NCDFE when loading <code>foo.A</code> * because <code>bar.B</code> cannot be found). * * @param bundle the bundle to search for (and which should do the loading) * @param className the name of the class that failed to be loaded in dot * format (i.e. java.lang.Thread) * @param rootClassName the name of the class that triggered the loading * (i.e. java.lang.Runnable) */ public static void debugClassLoading(Bundle bundle, String className, String rootClassName) { boolean trace = log.isTraceEnabled(); if (!trace) return; Dictionary dict = bundle.getHeaders(); String bname = dict.get(Constants.BUNDLE_NAME) + "(" + dict.get(Constants.BUNDLE_SYMBOLICNAME) + ")"; if (trace) log.trace("Could not find class [" + className + "] required by [" + bname + "] scanning available bundles"); BundleContext context = OsgiBundleUtils.getBundleContext(bundle); int pkgIndex = className.lastIndexOf('.'); // Reject global packages if (pkgIndex < 0) { if (trace) log.trace("Class is not in a package, its unlikely that this will work"); return; } String packageName = className.substring(0, pkgIndex); Version iversion = hasImport(bundle, packageName); if (iversion != null && context != null) { if (trace) log.trace("Class is correctly imported as version [" + iversion + "], checking providing bundles"); Bundle[] bundles = context.getBundles(); for (int i = 0; i < bundles.length; i++) { if (bundles[i].getBundleId() != bundle.getBundleId()) { Version exported = checkBundleForClass(bundles[i], className, iversion); // Everything looks ok, but is the root bundle importing the // dependent class also? if (exported != null && exported.equals(iversion) && rootClassName != null) { for (int j = 0; j < bundles.length; j++) { Version rootexport = hasExport(bundles[j], rootClassName.substring(0, rootClassName.lastIndexOf('.'))); if (rootexport != null) { // TODO -- this is very rough, check the bundle // classpath also. Version rootimport = hasImport(bundles[j], packageName); if (rootimport == null || !rootimport.equals(iversion)) { if (trace) log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundles[j]) + "] exports [" + rootClassName + "] as version [" + rootexport + "] but does not import dependent package [" + packageName + "] at version [" + iversion + "]"); } } } } } } } if (hasExport(bundle, packageName) != null) { if (trace) log.trace("Class is exported, checking this bundle"); checkBundleForClass(bundle, className, iversion); } } private static Version checkBundleForClass(Bundle bundle, String name, Version iversion) { String packageName = name.substring(0, name.lastIndexOf('.')); Version hasExport = hasExport(bundle, packageName); // log.info("Examining Bundle [" + bundle.getBundleId() + ": " + bname + // "]"); // Check for version matching if (hasExport != null && !hasExport.equals(iversion)) { log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] exports [" + packageName + "] as version [" + hasExport + "] but version [" + iversion + "] was required"); return hasExport; } // Do more detailed checks String cname = name.substring(packageName.length() + 1) + ".class"; Enumeration e = bundle.findEntries("/" + packageName.replace('.', '/'), cname, false); if (e == null) { if (hasExport != null) { URL url = checkBundleJarsForClass(bundle, name); if (url != null) { log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] contains [" + cname + "] in embedded jar [" + url.toString() + "] but exports the package"); } else { log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] does not contain [" + cname + "] but exports the package"); } } String root = "/"; String fileName = packageName; if (packageName.lastIndexOf(".") >= 0) { root = root + packageName.substring(0, packageName.lastIndexOf(".")).replace('.', '/'); fileName = packageName.substring(packageName.lastIndexOf(".") + 1).replace('.', '/'); } Enumeration pe = bundle.findEntries(root, fileName, false); if (pe != null) { if (hasExport != null) { log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] contains package [" + packageName + "] and exports it"); } else { log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] contains package [" + packageName + "] but does not export it"); } } } // Found the resource, check that it is exported. else { if (hasExport != null) { log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] contains resource [" + cname + "] and it is correctly exported as version [" + hasExport + "]"); Class<?> c = null; try { c = bundle.loadClass(name); } catch (ClassNotFoundException e1) { // Ignored } log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] loadClass [" + cname + "] returns [" + c + "]"); } else { log.trace("Bundle [" + OsgiStringUtils.nullSafeNameAndSymName(bundle) + "] contains resource [" + cname + "] but its package is not exported"); } } return hasExport; } private static URL checkBundleJarsForClass(Bundle bundle, String name) { String cname = name.replace('.', '/') + ".class"; for (Enumeration e = bundle.findEntries("/", "*.jar", true); e != null && e.hasMoreElements();) { URL url = (URL) e.nextElement(); JarInputStream jin = null; try { jin = new JarInputStream(url.openStream()); // Copy entries from the real jar to our virtual jar for (JarEntry ze = jin.getNextJarEntry(); ze != null; ze = jin.getNextJarEntry()) { if (ze.getName().equals(cname)) { jin.close(); return url; } } } catch (IOException e1) { log.trace("Skipped " + url.toString() + ": " + e1.getMessage()); } finally { if (jin != null) { try { jin.close(); } catch (Exception ex) { // ignore it } } } } return null; } /** * Get the version of a package import from a bundle. * * @param bundle * @param packageName * @return */ private static Version hasImport(Bundle bundle, String packageName) { Dictionary dict = bundle.getHeaders(); // Check imports String imports = (String) dict.get(Constants.IMPORT_PACKAGE); Version v = getVersion(imports, packageName); if (v != null) { return v; } // Check for dynamic imports String dynimports = (String) dict.get(Constants.DYNAMICIMPORT_PACKAGE); if (dynimports != null) { for (StringTokenizer strok = new StringTokenizer(dynimports, COMMA); strok.hasMoreTokens();) { StringTokenizer parts = new StringTokenizer(strok.nextToken(), SEMI_COLON); String pkg = parts.nextToken().trim(); if (pkg.endsWith(".*") && packageName.startsWith(pkg.substring(0, pkg.length() - 2)) || pkg.equals("*")) { Version version = Version.emptyVersion; for (; parts.hasMoreTokens();) { String modifier = parts.nextToken().trim(); if (modifier.startsWith("version")) { version = Version.parseVersion(modifier.substring(modifier.indexOf(EQUALS) + 1).trim()); } } return version; } } } return null; } private static Version hasExport(Bundle bundle, String packageName) { Dictionary dict = bundle.getHeaders(); return getVersion((String) dict.get(Constants.EXPORT_PACKAGE), packageName); } /** * Get the version of a package name. * * @param stmt * @param packageName * @return */ private static Version getVersion(String stmt, String packageName) { if (stmt != null) { String[] pkgs = splitIntoPackages(stmt); for (int packageIndex = 0; packageIndex < pkgs.length; packageIndex++) { String pkgToken = pkgs[packageIndex].trim(); String pkg = null; Version version = null; int firstDirectiveIndex = pkgToken.indexOf(SEMI_COLON); if (firstDirectiveIndex > -1) { pkg = pkgToken.substring(0, firstDirectiveIndex); } else { pkg = pkgToken; version = Version.emptyVersion; } // check for version only if we have a match if (pkg.equals(packageName)) { // no version determined, find one if (version == null) { String[] directiveTokens = pkgToken.substring(firstDirectiveIndex + 1).split(SEMI_COLON); for (int directiveTokenIndex = 0; directiveTokenIndex < directiveTokens.length; directiveTokenIndex++) { String directive = directiveTokens[directiveTokenIndex].trim(); // found it if (directive.startsWith(Constants.VERSION_ATTRIBUTE)) { String value = directive.substring(directive.indexOf(EQUALS) + 1).trim(); boolean lowEqualTo = value.startsWith("\"["); boolean lowGreaterThen = value.startsWith("\"("); if (lowEqualTo || lowGreaterThen) { boolean highEqualTo = value.endsWith("]\""); boolean highLessThen = value.endsWith(")\""); // remove brackets value = value.substring(2, value.length() - 2); int commaIndex = value.indexOf(COMMA); // TODO: currently, only the left side is considered Version left = Version.parseVersion(value.substring(0, commaIndex)); Version right = Version.parseVersion(value.substring(commaIndex + 1)); return left; } // check quotes if (value.startsWith("\"")) { return Version.parseVersion(value.substring(1, value.length() - 1)); } return Version.parseVersion(value); } } if (version == null) { version = Version.emptyVersion; } } return version; } } } return null; } private static String[] splitIntoPackages(String stmt) { // spit the statement into packages but consider " List pkgs = new ArrayList(2); StringBuilder pkg = new StringBuilder(); boolean ignoreComma = false; for (int stringIndex = 0; stringIndex < stmt.length(); stringIndex++) { char currentChar = stmt.charAt(stringIndex); if (currentChar == ',') { if (ignoreComma) { pkg.append(currentChar); } else { pkgs.add(pkg.toString()); pkg = new StringBuilder(); ignoreComma = false; } } else { if (currentChar == '\"') { ignoreComma = !ignoreComma; } pkg.append(currentChar); } } pkgs.add(pkg.toString()); return (String[]) pkgs.toArray(new String[pkgs.size()]); } }