com.google.gwt.dev.shell.ModuleSpace.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.dev.shell.ModuleSpace.java

Source

/*
 * Copyright 2008 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.dev.shell;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.util.Name;
import com.google.gwt.dev.util.Name.BinaryName;
import com.google.gwt.dev.util.Util;
import com.google.gwt.dev.util.log.speedtracer.DevModeEventType;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;

/**
 * The interface to the low-level browser, this class serves as a 'domain' for a
 * module, loading all of its classes in a separate, isolated class loader. This
 * allows us to run multiple modules, both in succession and simultaneously.
 */
public abstract class ModuleSpace implements ShellJavaScriptHost {
    private static ThreadLocal<Throwable> sCaughtJavaExceptionObject = new ThreadLocal<Throwable>();

    private static ThreadLocal<Throwable> sThrownJavaExceptionObject = new ThreadLocal<Throwable>();

    /**
     * Logger is thread local.
     */
    private static ThreadLocal<TreeLogger> threadLocalLogger = new ThreadLocal<TreeLogger>();

    public static void setThrownJavaException(Throwable t) {
        sThrownJavaExceptionObject.set(t);
    }

    /**
     * Equivalent to
     * {@link #createJavaScriptException(ClassLoader,Object,String)
     * createJavaScriptException(cl, exception, "")}.
     */
    protected static RuntimeException createJavaScriptException(ClassLoader cl, Object exception) {
        return createJavaScriptException(cl, exception, "");
    }

    /**
     * Create a JavaScriptException object. This must be done reflectively,
     * since this class will have been loaded from a ClassLoader other than the
     * session's thread.
     */
    protected static RuntimeException createJavaScriptException(ClassLoader cl, Object exception, String message) {
        Exception caught;
        try {
            Class<?> javaScriptExceptionClass = Class.forName("com.google.gwt.core.client.JavaScriptException",
                    true, cl);
            Constructor<?> ctor = javaScriptExceptionClass.getDeclaredConstructor(Object.class, String.class);
            return (RuntimeException) ctor.newInstance(new Object[] { exception, message });
        } catch (InstantiationException e) {
            caught = e;
        } catch (IllegalAccessException e) {
            caught = e;
        } catch (SecurityException e) {
            caught = e;
        } catch (ClassNotFoundException e) {
            caught = e;
        } catch (NoSuchMethodException e) {
            caught = e;
        } catch (IllegalArgumentException e) {
            caught = e;
        } catch (InvocationTargetException e) {
            caught = e;
        }
        throw new RuntimeException("Error creating JavaScriptException", caught);
    }

    protected static TreeLogger getLogger() {
        return threadLocalLogger.get();
    }

    /**
     * Get the original thrown object. If the exception is JavaScriptException,
     * gets the object wrapped by a JavaScriptException. We have to do this
     * reflectively, since the JavaScriptException object is from an arbitrary
     * classloader. If the object is not a JavaScriptException, or is not from
     * the given ClassLoader, or the exception is not set we'll return exception
     * itself.
     */
    static Object getThrownObject(ClassLoader cl, Object exception) {
        if (exception.getClass().getClassLoader() != cl) {
            return exception;
        }
        Exception caught;
        try {
            Class<?> javaScriptExceptionClass = Class.forName("com.google.gwt.core.client.JavaScriptException",
                    true, cl);
            if (!javaScriptExceptionClass.isInstance(exception)) {
                // Not a JavaScriptException
                return exception;
            }
            Method isThrownSet = javaScriptExceptionClass.getMethod("isThrownSet");
            if (!((Boolean) isThrownSet.invoke(exception))) {
                return exception;
            }
            Method getThrown = javaScriptExceptionClass.getMethod("getThrown");
            return getThrown.invoke(exception);
        } catch (NoSuchMethodException e) {
            caught = e;
        } catch (ClassNotFoundException e) {
            caught = e;
        } catch (IllegalArgumentException e) {
            caught = e;
        } catch (IllegalAccessException e) {
            caught = e;
        } catch (InvocationTargetException e) {
            caught = e;
        }
        throw new RuntimeException("Error getting exception value", caught);
    }

    protected final ModuleSpaceHost host;

    private final TreeLogger logger;

    private final String moduleName;

    protected ModuleSpace(TreeLogger logger, ModuleSpaceHost host, String moduleName) {
        this.host = host;
        this.moduleName = moduleName;
        this.logger = logger;
        threadLocalLogger.set(host.getLogger());
    }

    public void dispose() {
        // Clear our class loader.
        getIsolatedClassLoader().clear();
    }

    @Override
    public void exceptionCaught(Object exception) {
        Throwable caught;
        Throwable thrown = sThrownJavaExceptionObject.get();
        if (thrown != null && isExceptionSame(thrown, exception)) {
            // The caught exception was thrown by us.
            caught = thrown;
            sThrownJavaExceptionObject.set(null);
        } else if (exception instanceof Throwable) {
            caught = (Throwable) exception;
        } else {
            caught = createJavaScriptException(getIsolatedClassLoader(), exception);
            // Remove excess stack frames from the new exception.
            caught.fillInStackTrace();
            StackTraceElement[] trace = caught.getStackTrace();
            assert trace.length > 1;
            assert trace[1].getClassName().equals(JavaScriptHost.class.getName());
            assert trace[1].getMethodName().equals("exceptionCaught");
            StackTraceElement[] newTrace = new StackTraceElement[trace.length - 1];
            System.arraycopy(trace, 1, newTrace, 0, newTrace.length);
            caught.setStackTrace(newTrace);
        }
        sCaughtJavaExceptionObject.set(caught);
    }

    /**
     * Get the module name.
     *
     * @return the module name
     */
    public String getModuleName() {
        return moduleName;
    }

    @Override
    public boolean invokeNativeBoolean(String name, Object jthis, Class<?>[] types, Object[] args)
            throws Throwable {
        JsValue result = invokeNative(name, jthis, types, args);
        String msgPrefix = composeResultErrorMsgPrefix(name, "a boolean");
        Boolean value = JsValueGlue.get(result, getIsolatedClassLoader(), boolean.class, msgPrefix);
        if (value == null) {
            throw new HostedModeException(msgPrefix + ": return value null received, expected a boolean");
        }
        return value.booleanValue();
    }

    @Override
    public byte invokeNativeByte(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable {
        JsValue result = invokeNative(name, jthis, types, args);
        String msgPrefix = composeResultErrorMsgPrefix(name, "a byte");
        Byte value = JsValueGlue.get(result, null, Byte.TYPE, msgPrefix);
        if (value == null) {
            throw new HostedModeException(msgPrefix + ": return value null received, expected a byte");
        }
        return value.byteValue();
    }

    @Override
    public char invokeNativeChar(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable {
        JsValue result = invokeNative(name, jthis, types, args);
        String msgPrefix = composeResultErrorMsgPrefix(name, "a char");
        Character value = JsValueGlue.get(result, null, Character.TYPE, msgPrefix);
        if (value == null) {
            throw new HostedModeException(msgPrefix + ": return value null received, expected a char");
        }
        return value.charValue();
    }

    @Override
    public double invokeNativeDouble(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable {
        JsValue result = invokeNative(name, jthis, types, args);
        String msgPrefix = composeResultErrorMsgPrefix(name, "a double");
        Double value = JsValueGlue.get(result, null, Double.TYPE, msgPrefix);
        if (value == null) {
            throw new HostedModeException(msgPrefix + ": return value null received, expected a double");
        }
        return value.doubleValue();
    }

    @Override
    public float invokeNativeFloat(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable {
        JsValue result = invokeNative(name, jthis, types, args);
        String msgPrefix = composeResultErrorMsgPrefix(name, "a float");
        Float value = JsValueGlue.get(result, null, Float.TYPE, msgPrefix);
        if (value == null) {
            throw new HostedModeException(msgPrefix + ": return value null received, expected a float");
        }
        return value.floatValue();
    }

    @Override
    public int invokeNativeInt(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable {
        JsValue result = invokeNative(name, jthis, types, args);
        String msgPrefix = composeResultErrorMsgPrefix(name, "an int");
        Integer value = JsValueGlue.get(result, null, Integer.TYPE, msgPrefix);
        if (value == null) {
            throw new HostedModeException(msgPrefix + ": return value null received, expected an int");
        }
        return value.intValue();
    }

    @Override
    public long invokeNativeLong(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable {
        JsValue result = invokeNative(name, jthis, types, args);
        String msgPrefix = composeResultErrorMsgPrefix(name, "a long");
        Long value = JsValueGlue.get(result, null, Long.TYPE, msgPrefix);
        if (value == null) {
            throw new HostedModeException(msgPrefix + ": return value null received, expected a long");
        }
        return value.longValue();
    }

    @Override
    public Object invokeNativeObject(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable {
        JsValue result = invokeNative(name, jthis, types, args);
        String msgPrefix = composeResultErrorMsgPrefix(name, "a Java object");
        return JsValueGlue.get(result, getIsolatedClassLoader(), Object.class, msgPrefix);
    }

    @Override
    public short invokeNativeShort(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable {
        JsValue result = invokeNative(name, jthis, types, args);
        String msgPrefix = composeResultErrorMsgPrefix(name, "a short");
        Short value = JsValueGlue.get(result, null, Short.TYPE, msgPrefix);
        if (value == null) {
            throw new HostedModeException(msgPrefix + ": return value null received, expected a short");
        }
        return value.shortValue();
    }

    @Override
    public void invokeNativeVoid(String name, Object jthis, Class<?>[] types, Object[] args) throws Throwable {
        JsValue result = invokeNative(name, jthis, types, args);
        if (!result.isUndefined()) {
            logger.log(TreeLogger.WARN, "JSNI method '" + name + "' returned a value of type "
                    + result.getTypeString() + " but was declared void; it should not have returned a value at all",
                    null);
        }
    }

    /**
     * Allows client-side code to log to the tree logger.
     */
    @Override
    public void log(String message, Throwable e) {
        TreeLogger.Type type = TreeLogger.INFO;
        if (e != null) {
            type = TreeLogger.ERROR;
        }
        // Log at the top level for visibility.
        TreeLogger t = getLogger();
        if (t != null) {
            getLogger().log(type, message, e);
        }
    }

    /**
     * Runs the module's user startup code.
     */
    public final void onLoad(TreeLogger logger) throws UnableToCompleteException {
        Event moduleSpaceLoadEvent = SpeedTracerLogger.start(DevModeEventType.MODULE_SPACE_LOAD);
        // Tell the host we're ready for business.
        //
        host.onModuleReady(this);
        // Make sure we can resolve JSNI references to static Java names.
        //
        try {
            createStaticDispatcher(logger);
            Object staticDispatch = getStaticDispatcher();
            invokeNativeVoid("__defineStatic", null, new Class[] { Object.class }, new Object[] { staticDispatch });
        } catch (Throwable e) {
            logger.log(TreeLogger.ERROR, "Unable to initialize static dispatcher", e);
            throw new UnableToCompleteException();
        }
        // Actually run user code.
        //
        String entryPointTypeName = null;
        try {
            // Set up GWT-entry code
            Class<?> implClass = loadClassFromSourceName("com.google.gwt.core.client.impl.Impl");
            Method registerEntry = implClass.getDeclaredMethod("registerEntry");
            registerEntry.setAccessible(true);
            registerEntry.invoke(null);
            Method enter = implClass.getDeclaredMethod("enter");
            enter.setAccessible(true);
            enter.invoke(null);
            String[] entryPoints = host.getEntryPointTypeNames();
            if (entryPoints.length > 0) {
                try {
                    for (int i = 0; i < entryPoints.length; i++) {
                        entryPointTypeName = entryPoints[i];
                        Method onModuleLoad = null;
                        Object module;
                        // Try to initialize EntryPoint, else throw up glass
                        // panel
                        try {
                            Class<?> clazz = loadClassFromSourceName(entryPointTypeName);
                            try {
                                onModuleLoad = clazz.getMethod("onModuleLoad");
                                if (!Modifier.isStatic(onModuleLoad.getModifiers())) {
                                    // it's non-static, so we need to rebind the
                                    // class
                                    onModuleLoad = null;
                                }
                            } catch (NoSuchMethodException e) {
                                // okay, try rebinding it; maybe the rebind
                                // result will have one
                            }
                            module = null;
                            if (onModuleLoad == null) {
                                module = rebindAndCreate(entryPointTypeName);
                                onModuleLoad = module.getClass().getMethod("onModuleLoad");
                                // Record the rebound name of the class for
                                // stats (below).
                                entryPointTypeName = module.getClass().getName().replace('$', '.');
                            }
                        } catch (Throwable e) {
                            displayErrorGlassPanel("EntryPoint initialization exception", entryPointTypeName, e);
                            throw e;
                        }
                        // Try to invoke onModuleLoad, else throw up glass panel
                        try {
                            onModuleLoad.setAccessible(true);
                            invokeNativeVoid("fireOnModuleLoadStart", null, new Class[] { String.class },
                                    new Object[] { entryPointTypeName });
                            Event onModuleLoadEvent = SpeedTracerLogger.start(DevModeEventType.ON_MODULE_LOAD);
                            try {
                                onModuleLoad.invoke(module);
                            } finally {
                                onModuleLoadEvent.end();
                            }
                        } catch (Throwable e) {
                            displayErrorGlassPanel("onModuleLoad() threw an exception", entryPointTypeName, e);
                            throw e;
                        }
                    }
                } finally {
                    Method exit = implClass.getDeclaredMethod("exit", boolean.class);
                    exit.setAccessible(true);
                    exit.invoke(null, true);
                }
            } else {
                logger.log(TreeLogger.WARN,
                        "The module has no entry points defined, so onModuleLoad() will never be called", null);
            }
        } catch (Throwable e) {
            Throwable caught = e;
            if (e instanceof InvocationTargetException) {
                caught = ((InvocationTargetException) e).getTargetException();
            }
            if (caught instanceof ExceptionInInitializerError) {
                caught = ((ExceptionInInitializerError) caught).getException();
            }
            String unableToLoadMessage = "Unable to load module entry point class " + entryPointTypeName;
            if (caught != null) {
                unableToLoadMessage += " (see associated exception for details)";
            }
            // logger.log(TreeLogger.ERROR, unableToLoadMessage, caught);
            // plugin issue
            logger.log(TreeLogger.INFO, unableToLoadMessage);
            caught.printStackTrace();
            throw new UnableToCompleteException();
        } finally {
            moduleSpaceLoadEvent.end();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T rebindAndCreate(String requestedClassName) throws UnableToCompleteException {
        assert Name.isBinaryName(requestedClassName);
        Throwable caught = null;
        String msg = null;
        String resultName = null;
        Class<?> resolvedClass = null;
        Event moduleSpaceRebindAndCreate = SpeedTracerLogger.start(DevModeEventType.MODULE_SPACE_REBIND_AND_CREATE);
        try {
            // Rebind operates on source-level names.
            //
            String sourceName = BinaryName.toSourceName(requestedClassName);
            resultName = rebind(sourceName);
            moduleSpaceRebindAndCreate.addData("Requested Class", requestedClassName, "Result Name", resultName);
            resolvedClass = loadClassFromSourceName(resultName);
            if (Modifier.isAbstract(resolvedClass.getModifiers())) {
                msg = "Deferred binding result type '" + resultName + "' should not be abstract";
            } else {
                Constructor<?> ctor = resolvedClass.getDeclaredConstructor();
                ctor.setAccessible(true);
                return (T) ctor.newInstance();
            }
        } catch (ClassNotFoundException e) {
            msg = "Could not load deferred binding result type '" + resultName + "'";
            caught = e;
        } catch (InstantiationException e) {
            caught = e;
        } catch (IllegalAccessException e) {
            caught = e;
        } catch (ExceptionInInitializerError e) {
            caught = e.getException();
        } catch (NoSuchMethodException e) {
            // If it is a nested class and not declared as static,
            // then it's not accessible from outside.
            //
            if (resolvedClass.getEnclosingClass() != null && !Modifier.isStatic(resolvedClass.getModifiers())) {
                msg = "Rebind result '" + resultName + " is a non-static inner class";
            } else {
                msg = "Rebind result '" + resultName + "' has no default (zero argument) constructors.";
            }
            caught = e;
        } catch (InvocationTargetException e) {
            caught = e.getTargetException();
        } finally {
            moduleSpaceRebindAndCreate.end();
        }
        // Always log here because sometimes this method gets called from static
        // initializers and other unusual places, which can obscure the problem.
        //
        if (msg == null) {
            msg = "Failed to create an instance of '" + requestedClassName + "' via deferred binding ";
        }
        caught.printStackTrace();
        GWT.log(msg, caught);
        host.getLogger().log(TreeLogger.ERROR, msg, caught);
        throw new UnableToCompleteException();
    }

    private String composeResultErrorMsgPrefix(String name, String typePhrase) {
        return "Something other than " + typePhrase + " was returned from JSNI method '" + name + "'";
    }

    private void displayErrorGlassPanel(String summary, String entryPointTypeName, Throwable e) throws Throwable {
        StringWriter writer = new StringWriter();
        e.printStackTrace(new PrintWriter(writer));
        String stackTrace = Util.escapeXml(writer.toString()).replaceFirst(
                // (?ms) for regex pattern modifiers MULTILINE and DOTALL
                "(?ms)(Caused by:.+)", "<b>$1</b>");
        String details = "<p>Exception while loading module <b>" + Util.escapeXml(entryPointTypeName) + "</b>."
                + " See Development Mode for details.</p>" + "<div style='overflow:visible;white-space:pre;'>"
                + stackTrace + "</div>";
        invokeNativeVoid("__gwt_displayGlassMessage", null, new Class[] { String.class, String.class },
                new Object[] { Util.escapeXml(summary), details });
    }

    private boolean isUserFrame(StackTraceElement element) {
        try {
            CompilingClassLoader cl = getIsolatedClassLoader();
            String className = element.getClassName();
            Class<?> clazz = Class.forName(className, false, cl);
            if (clazz.getClassLoader() == cl) {
                // Lives in user classLoader.
                return true;
            }
            // At this point, it must be a JRE class to qualify.
            if (clazz.getClassLoader() != null || !className.startsWith("java.")) {
                return false;
            }
            if (className.startsWith("java.lang.reflect.")) {
                return false;
            }
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

    /**
     * Handles loading a class that might be nested given a source type name.
     */
    private Class<?> loadClassFromSourceName(String sourceName) throws ClassNotFoundException {
        Event moduleSpaceClassLoad = SpeedTracerLogger.start(DevModeEventType.MODULE_SPACE_CLASS_LOAD,
                "Source Name", sourceName);
        try {
            String toTry = sourceName;
            while (true) {
                try {
                    return Class.forName(toTry, true, getIsolatedClassLoader());
                } catch (ClassNotFoundException e) {
                    // Assume that the last '.' should be '$' and try again.
                    //
                    int i = toTry.lastIndexOf('.');
                    if (i == -1) {
                        throw e;
                    }
                    toTry = toTry.substring(0, i) + "$" + toTry.substring(i + 1);
                }
            }
        } finally {
            moduleSpaceClassLoad.end();
        }
    }

    /**
     * Clean up the stack trace by removing our hosting frames. But don't do
     * this if our own frames are at the top of the stack, because we may be the
     * real cause of the exception.
     */
    private void scrubStackTrace(Throwable thrown) {
        List<StackTraceElement> trace = new ArrayList<StackTraceElement>(Arrays.asList(thrown.getStackTrace()));
        boolean seenUserFrame = false;
        for (ListIterator<StackTraceElement> it = trace.listIterator(); it.hasNext();) {
            StackTraceElement element = it.next();
            if (!isUserFrame(element)) {
                if (seenUserFrame) {
                    it.remove();
                }
                continue;
            }
            seenUserFrame = true;
            // Remove a JavaScriptHost.invokeNative*() frame.
            if (element.getClassName().equals(JavaScriptHost.class.getName())) {
                if (element.getMethodName().equals("exceptionCaught")) {
                    it.remove();
                } else if (element.getMethodName().startsWith("invokeNative")) {
                    it.remove();
                    // Also try to convert the next frame to a true native.
                    if (it.hasNext()) {
                        StackTraceElement next = it.next();
                        if (next.getLineNumber() == -1) {
                            next = new StackTraceElement(next.getClassName(), next.getMethodName(),
                                    next.getFileName(), -2);
                            it.set(next);
                        }
                    }
                }
            }
        }
        thrown.setStackTrace(trace.toArray(new StackTraceElement[trace.size()]));
    }

    /**
     * Create the __defineStatic method.
     *
     * @param logger
     */
    protected abstract void createStaticDispatcher(TreeLogger logger);

    /**
     * Invokes a native JavaScript function.
     *
     * @param name
     *            the name of the function to invoke
     * @param jthis
     *            the function's 'this' context
     * @param types
     *            the type of each argument
     * @param args
     *            the arguments to be passed
     * @return the return value as a Variant.
     */
    protected abstract JsValue doInvoke(String name, Object jthis, Class<?>[] types, Object[] args)
            throws Throwable;

    protected CompilingClassLoader getIsolatedClassLoader() {
        return host.getClassLoader();
    }

    /**
     * Injects the magic needed to resolve JSNI references from module-space.
     */
    protected abstract Object getStaticDispatcher();

    /**
     * Invokes a native JavaScript function.
     *
     * @param name
     *            the name of the function to invoke
     * @param jthis
     *            the function's 'this' context
     * @param types
     *            the type of each argument
     * @param args
     *            the arguments to be passed
     * @return the return value as a Variant.
     */
    protected final JsValue invokeNative(String name, Object jthis, Class<?>[] types, Object[] args)
            throws Throwable {
        JsValue result = doInvoke(name, jthis, types, args);
        // Is an exception active?
        Throwable thrown = sCaughtJavaExceptionObject.get();
        if (thrown == null) {
            return result;
        }
        sCaughtJavaExceptionObject.set(null);
        scrubStackTrace(thrown);
        throw thrown;
    }

    /**
     * @param original
     *            the thrown exception
     * @param exception
     *            the caught exception
     */
    protected boolean isExceptionSame(Throwable original, Object exception) {
        // For most platforms, the null exception means we threw it.
        // IE overrides this.
        return exception == null;
    }

    protected String rebind(String sourceName) throws UnableToCompleteException {
        try {
            String result = host.rebind(logger, sourceName);
            if (result != null) {
                return result;
            } else {
                return sourceName;
            }
        } catch (UnableToCompleteException e) {
            String msg = "Deferred binding failed for '" + sourceName + "'; expect subsequent failures";
            host.getLogger().log(TreeLogger.ERROR, msg);
            throw e;
        }
    }
}