org.eclipse.gemini.blueprint.util.DebugUtils.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.gemini.blueprint.util.DebugUtils.java

Source

/******************************************************************************
 * 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()]);
    }
}