001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.dev.shell;
017:
018: import com.google.gwt.core.ext.TreeLogger;
019: import com.google.gwt.core.ext.UnableToCompleteException;
020:
021: import java.lang.reflect.Constructor;
022: import java.lang.reflect.InvocationTargetException;
023: import java.lang.reflect.Method;
024: import java.lang.reflect.Modifier;
025:
026: /**
027: * The interface to the low-level browser, this class serves as a 'domain' for a
028: * module, loading all of its classes in a separate, isolated class loader. This
029: * allows us to run multiple modules, both in succession and simultaneously.
030: */
031: public abstract class ModuleSpace implements ShellJavaScriptHost {
032:
033: private static ThreadLocal<Throwable> sCaughtJavaExceptionObject = new ThreadLocal<Throwable>();
034:
035: private static ThreadLocal<Throwable> sLastThrownJavaException = new ThreadLocal<Throwable>();
036:
037: private static ThreadLocal<Throwable> sThrownJavaExceptionObject = new ThreadLocal<Throwable>();
038:
039: /**
040: * Logger is thread local.
041: */
042: private static ThreadLocal<TreeLogger> threadLocalLogger = new ThreadLocal<TreeLogger>();
043:
044: public static void setThrownJavaException(Throwable t) {
045: Throwable was = sLastThrownJavaException.get();
046: if (was != t) {
047: // avoid logging the same exception twice
048: getLogger().log(TreeLogger.WARN,
049: "Exception thrown into JavaScript", t);
050: sLastThrownJavaException.set(t);
051: }
052: sThrownJavaExceptionObject.set(t);
053: }
054:
055: protected static RuntimeException createJavaScriptException(
056: ClassLoader cl, String name, String desc) {
057: Exception caught;
058: try {
059: Class<?> javaScriptExceptionClass = Class.forName(
060: "com.google.gwt.core.client.JavaScriptException",
061: true, cl);
062: Class<?> string = String.class;
063: Constructor<?> ctor = javaScriptExceptionClass
064: .getDeclaredConstructor(new Class<?>[] { string,
065: string });
066: return (RuntimeException) ctor.newInstance(new Object[] {
067: name, desc });
068: } catch (InstantiationException e) {
069: caught = e;
070: } catch (IllegalAccessException e) {
071: caught = e;
072: } catch (SecurityException e) {
073: caught = e;
074: } catch (ClassNotFoundException e) {
075: caught = e;
076: } catch (NoSuchMethodException e) {
077: caught = e;
078: } catch (IllegalArgumentException e) {
079: caught = e;
080: } catch (InvocationTargetException e) {
081: caught = e;
082: }
083: throw new RuntimeException(
084: "Error creating JavaScriptException", caught);
085: }
086:
087: protected static TreeLogger getLogger() {
088: return threadLocalLogger.get();
089: }
090:
091: /**
092: * Tricky one, this. Reaches over into this modules's JavaScriptHost class and
093: * sets its static 'host' field to be the specified ModuleSpace instance
094: * (which will either be this ModuleSpace or null).
095: *
096: * @param moduleSpace the ModuleSpace instance to store using
097: * JavaScriptHost.setHost().
098: * @see JavaScriptHost
099: */
100: private static void setJavaScriptHost(ModuleSpace moduleSpace,
101: ClassLoader cl) {
102: // Find the application's JavaScriptHost interface.
103: //
104: Throwable caught;
105: try {
106: final String jsHostClassName = JavaScriptHost.class
107: .getName();
108: Class<?> jsHostClass = Class.forName(jsHostClassName, true,
109: cl);
110: final Class<?>[] paramTypes = new Class[] { ShellJavaScriptHost.class };
111: Method setHostMethod = jsHostClass.getMethod("setHost",
112: paramTypes);
113: setHostMethod.invoke(jsHostClass,
114: new Object[] { moduleSpace });
115: return;
116: } catch (ClassNotFoundException e) {
117: caught = e;
118: } catch (SecurityException e) {
119: caught = e;
120: } catch (NoSuchMethodException e) {
121: caught = e;
122: } catch (IllegalArgumentException e) {
123: caught = e;
124: } catch (IllegalAccessException e) {
125: caught = e;
126: } catch (InvocationTargetException e) {
127: caught = e.getTargetException();
128: }
129: throw new RuntimeException("Error initializing JavaScriptHost",
130: caught);
131: }
132:
133: private final ModuleSpaceHost host;
134:
135: private final Object key;
136:
137: private final String moduleName;
138:
139: protected ModuleSpace(ModuleSpaceHost host, String moduleName,
140: Object key) {
141: this .host = host;
142: this .moduleName = moduleName;
143: this .key = key;
144: TreeLogger hostLogger = host.getLogger();
145: threadLocalLogger.set(hostLogger);
146: }
147:
148: public void dispose() {
149: // Tell the user-space JavaScript host object that we're done
150: //
151: clearJavaScriptHost();
152:
153: // Clear out the exception field, it may be holding a user-space object
154: sLastThrownJavaException.set(null);
155:
156: // Clear out the class loader's cache
157: host.getClassLoader().clear();
158: }
159:
160: public void exceptionCaught(int number, String name, String message) {
161: Throwable thrown = sThrownJavaExceptionObject.get();
162:
163: if (thrown != null) {
164: // See if the caught exception was thrown by us
165: if (isExceptionSame(thrown, number, name, message)) {
166: sCaughtJavaExceptionObject.set(thrown);
167: sThrownJavaExceptionObject.set(null);
168: return;
169: }
170: }
171:
172: sCaughtJavaExceptionObject.set(createJavaScriptException(
173: getIsolatedClassLoader(), name, message));
174: }
175:
176: /**
177: * Get the unique key for this module.
178: *
179: * @return the unique key
180: */
181: public Object getKey() {
182: return key;
183: }
184:
185: /**
186: * Get the module name.
187: *
188: * @return the module name
189: */
190: public String getModuleName() {
191: return moduleName;
192: }
193:
194: public boolean invokeNativeBoolean(String name, Object jthis ,
195: Class<?>[] types, Object[] args) throws Throwable {
196: JsValue result = invokeNative(name, jthis , types, args);
197: Boolean value = JsValueGlue.get(result, Boolean.class,
198: "invokeNativeBoolean(" + name + ")");
199: return value.booleanValue();
200: }
201:
202: public byte invokeNativeByte(String name, Object jthis ,
203: Class<?>[] types, Object[] args) throws Throwable {
204: JsValue result = invokeNative(name, jthis , types, args);
205: Byte value = JsValueGlue.get(result, Byte.class,
206: "invokeNativeByte(" + name + ")");
207: return value.byteValue();
208: }
209:
210: public char invokeNativeChar(String name, Object jthis ,
211: Class<?>[] types, Object[] args) throws Throwable {
212: JsValue result = invokeNative(name, jthis , types, args);
213: Character value = JsValueGlue.get(result, Character.class,
214: "invokeNativeCharacter(" + name + ")");
215: return value.charValue();
216: }
217:
218: public double invokeNativeDouble(String name, Object jthis ,
219: Class<?>[] types, Object[] args) throws Throwable {
220: JsValue result = invokeNative(name, jthis , types, args);
221: Double value = JsValueGlue.get(result, Double.class,
222: "invokeNativeDouble(" + name + ")");
223: return value.doubleValue();
224: }
225:
226: public float invokeNativeFloat(String name, Object jthis ,
227: Class<?>[] types, Object[] args) throws Throwable {
228: JsValue result = invokeNative(name, jthis , types, args);
229: Float value = JsValueGlue.get(result, Float.class,
230: "invokeNativeFloat(" + name + ")");
231: return value.floatValue();
232: }
233:
234: public Object invokeNativeHandle(String name, Object jthis ,
235: Class<?> returnType, Class<?>[] types, Object[] args)
236: throws Throwable {
237:
238: JsValue result = invokeNative(name, jthis , types, args);
239: return JsValueGlue.get(result, returnType,
240: "invokeNativeHandle(" + name + ")");
241: }
242:
243: public int invokeNativeInt(String name, Object jthis ,
244: Class<?>[] types, Object[] args) throws Throwable {
245: JsValue result = invokeNative(name, jthis , types, args);
246: Integer value = JsValueGlue.get(result, Integer.class,
247: "invokeNativeInteger(" + name + ")");
248: return value.intValue();
249: }
250:
251: public long invokeNativeLong(String name, Object jthis ,
252: Class<?>[] types, Object[] args) throws Throwable {
253: JsValue result = invokeNative(name, jthis , types, args);
254: Long value = JsValueGlue.get(result, Long.class,
255: "invokeNativeLong(" + name + ")");
256: return value.longValue();
257: }
258:
259: public Object invokeNativeObject(String name, Object jthis ,
260: Class<?>[] types, Object[] args) throws Throwable {
261: JsValue result = invokeNative(name, jthis , types, args);
262: return JsValueGlue.get(result, Object.class,
263: "invokeNativeObject(" + name + ")");
264: }
265:
266: public short invokeNativeShort(String name, Object jthis ,
267: Class<?>[] types, Object[] args) throws Throwable {
268: JsValue result = invokeNative(name, jthis , types, args);
269: Short value = JsValueGlue.get(result, Short.class,
270: "invokeNativeShort(" + name + ")");
271: return value.shortValue();
272: }
273:
274: public String invokeNativeString(String name, Object jthis ,
275: Class<?>[] types, Object[] args) throws Throwable {
276: JsValue result = invokeNative(name, jthis , types, args);
277: return JsValueGlue.get(result, String.class,
278: "invokeNativeString(" + name + ")");
279: }
280:
281: public void invokeNativeVoid(String name, Object jthis ,
282: Class<?>[] types, Object[] args) throws Throwable {
283: JsValue result = invokeNative(name, jthis , types, args);
284: if (!result.isUndefined()) {
285: getLogger().log(
286: TreeLogger.WARN,
287: "JSNI method '" + name
288: + "' returned a value of type "
289: + result.getTypeString()
290: + "; should not have returned a value",
291: null);
292: }
293: }
294:
295: /**
296: * Allows client-side code to log to the tree logger.
297: */
298: public void log(String message, Throwable e) {
299: TreeLogger logger = host.getLogger();
300: TreeLogger.Type type = TreeLogger.INFO;
301: if (e != null) {
302: type = TreeLogger.ERROR;
303: }
304: logger.log(type, message, e);
305: }
306:
307: /**
308: * Runs the module's user startup code.
309: */
310: public final void onLoad(TreeLogger logger)
311: throws UnableToCompleteException {
312: // Tell the host we're ready for business.
313: //
314: host.onModuleReady(this );
315:
316: // Tell the user-space JavaScript host object how to get back here.
317: //
318: setJavaScriptHost();
319:
320: // Make sure we can resolve JSNI references to static Java names.
321: //
322: try {
323: Object staticDispatch = getStaticDispatcher();
324: createNative("initializeStaticDispatcher", 0,
325: "__defineStatic", new String[] { "__arg0" },
326: "window.__static = __arg0;");
327: invokeNativeVoid("__defineStatic", null,
328: new Class[] { Object.class },
329: new Object[] { staticDispatch });
330: } catch (Throwable e) {
331: logger.log(TreeLogger.ERROR,
332: "Unable to initialize static dispatcher", e);
333: throw new UnableToCompleteException();
334: }
335:
336: // Actually run user code.
337: //
338: String entryPointTypeName = null;
339: try {
340: String[] entryPoints = host.getEntryPointTypeNames();
341: if (entryPoints.length > 0) {
342: for (int i = 0; i < entryPoints.length; i++) {
343: entryPointTypeName = entryPoints[i];
344: Class<?> clazz = loadClassFromSourceName(entryPointTypeName);
345: Method onModuleLoad = null;
346: try {
347: onModuleLoad = clazz.getMethod("onModuleLoad");
348: if (!Modifier.isStatic(onModuleLoad
349: .getModifiers())) {
350: // it's non-static, so we need to rebind the class
351: onModuleLoad = null;
352: }
353: } catch (NoSuchMethodException e) {
354: // okay, try rebinding it; maybe the rebind result will have one
355: }
356: Object module = null;
357: if (onModuleLoad == null) {
358: module = rebindAndCreate(entryPointTypeName);
359: onModuleLoad = module.getClass().getMethod(
360: "onModuleLoad");
361: }
362: onModuleLoad.setAccessible(true);
363: onModuleLoad.invoke(module);
364: }
365: } else {
366: logger
367: .log(
368: TreeLogger.WARN,
369: "The module has no entry points defined, so onModuleLoad() will never be called",
370: null);
371: }
372: } catch (Throwable e) {
373: Throwable caught = e;
374:
375: if (e instanceof InvocationTargetException) {
376: caught = ((InvocationTargetException) e)
377: .getTargetException();
378: }
379:
380: if (caught instanceof ExceptionInInitializerError) {
381: caught = ((ExceptionInInitializerError) caught)
382: .getException();
383: }
384:
385: String unableToLoadMessage = "Unable to load module entry point class "
386: + entryPointTypeName;
387: if (caught != null) {
388: unableToLoadMessage += " (see associated exception for details)";
389: }
390: logger.log(TreeLogger.ERROR, unableToLoadMessage, caught);
391: throw new UnableToCompleteException();
392: }
393: }
394:
395: public Object rebindAndCreate(String requestedClassName)
396: throws UnableToCompleteException {
397: Throwable caught = null;
398: String msg = null;
399: String resultName = null;
400: try {
401: // Rebind operates on source-level names.
402: //
403: String sourceName = requestedClassName.replace('$', '.');
404: resultName = rebind(sourceName);
405: Class<?> resolvedClass = loadClassFromSourceName(resultName);
406: if (Modifier.isAbstract(resolvedClass.getModifiers())) {
407: msg = "Deferred binding result type '" + resultName
408: + "' should not be abstract";
409: } else {
410: Constructor<?> ctor = resolvedClass
411: .getDeclaredConstructor();
412: ctor.setAccessible(true);
413: return ctor.newInstance();
414: }
415: } catch (ClassNotFoundException e) {
416: msg = "Could not load deferred binding result type '"
417: + resultName + "'";
418: caught = e;
419: } catch (InstantiationException e) {
420: caught = e;
421: } catch (IllegalAccessException e) {
422: caught = e;
423: } catch (ExceptionInInitializerError e) {
424: caught = e.getException();
425: } catch (NoSuchMethodException e) {
426: msg = "Rebind result '" + resultName
427: + "' has no default (zero argument) constructors.";
428: caught = e;
429: } catch (InvocationTargetException e) {
430: caught = e.getTargetException();
431: }
432:
433: // Always log here because sometimes this method gets called from static
434: // initializers and other unusual places, which can obscure the problem.
435: //
436: if (msg == null) {
437: msg = "Failed to create an instance of '"
438: + requestedClassName + "' via deferred binding ";
439: }
440: host.getLogger().log(TreeLogger.ERROR, msg, caught);
441: throw new UnableToCompleteException();
442: }
443:
444: protected String createNativeMethodInjector(String jsniSignature,
445: String[] paramNames, String js) {
446: String newScript = "window[\"" + jsniSignature
447: + "\"] = function(";
448:
449: for (int i = 0; i < paramNames.length; ++i) {
450: if (i > 0) {
451: newScript += ", ";
452: }
453:
454: newScript += paramNames[i];
455: }
456:
457: newScript += ") { " + js + " };\n";
458: return newScript;
459: }
460:
461: /**
462: * Invokes a native JavaScript function.
463: *
464: * @param name the name of the function to invoke
465: * @param jthis the function's 'this' context
466: * @param types the type of each argument
467: * @param args the arguments to be passed
468: * @return the return value as a Variant.
469: */
470: protected abstract JsValue doInvoke(String name, Object jthis ,
471: Class<?>[] types, Object[] args) throws Throwable;
472:
473: protected CompilingClassLoader getIsolatedClassLoader() {
474: return host.getClassLoader();
475: }
476:
477: /**
478: * Injects the magic needed to resolve JSNI references from module-space.
479: */
480: protected abstract Object getStaticDispatcher();
481:
482: /**
483: * Invokes a native JavaScript function.
484: *
485: * @param name the name of the function to invoke
486: * @param jthis the function's 'this' context
487: * @param types the type of each argument
488: * @param args the arguments to be passed
489: * @return the return value as a Variant.
490: */
491: protected final JsValue invokeNative(String name, Object jthis ,
492: Class<?>[] types, Object[] args) throws Throwable {
493: // Whenever a native method is invoked, release any enqueued cleanup objects
494: JsValue.mainThreadCleanup();
495: JsValue result = doInvoke(name, jthis , types, args);
496: // Is an exception active?
497: Throwable thrown = sCaughtJavaExceptionObject.get();
498: if (thrown == null) {
499: return result;
500: }
501: sCaughtJavaExceptionObject.set(null);
502:
503: /*
504: * The stack trace on the stored exception will not be very useful due to
505: * how it was created. Using fillInStackTrace() resets the stack trace to
506: * this moment in time, which is usually far more useful.
507: */
508: thrown.fillInStackTrace();
509: throw thrown;
510: }
511:
512: protected boolean isExceptionSame(Throwable original, int number,
513: String name, String message) {
514: // For most platforms, the null exception means we threw it.
515: // IE overrides this.
516: return (name == null && message == null);
517: }
518:
519: protected String rebind(String sourceName)
520: throws UnableToCompleteException {
521: try {
522: String result = host.rebind(host.getLogger(), sourceName);
523: if (result != null) {
524: return result;
525: } else {
526: return sourceName;
527: }
528: } catch (UnableToCompleteException e) {
529: String msg = "Deferred binding failed for '" + sourceName
530: + "'; expect subsequent failures";
531: host.getLogger().log(TreeLogger.ERROR, msg, e);
532: throw new UnableToCompleteException();
533: }
534: }
535:
536: /**
537: * Clear the module's JavaScriptHost 'host' field.
538: */
539: private void clearJavaScriptHost() {
540: setJavaScriptHost(null, getIsolatedClassLoader());
541: }
542:
543: /**
544: * Handles loading a class that might be nested given a source type name.
545: */
546: private Class<?> loadClassFromSourceName(String sourceName)
547: throws ClassNotFoundException {
548: String toTry = sourceName;
549: while (true) {
550: try {
551: return Class.forName(toTry, true,
552: getIsolatedClassLoader());
553: } catch (ClassNotFoundException e) {
554: // Assume that the last '.' should be '$' and try again.
555: //
556: int i = toTry.lastIndexOf('.');
557: if (i == -1) {
558: throw e;
559: }
560:
561: toTry = toTry.substring(0, i) + "$"
562: + toTry.substring(i + 1);
563: }
564: }
565: }
566:
567: /**
568: * Set the module's JavaScriptHost 'host' field to this ModuleSpace instance.
569: */
570: private void setJavaScriptHost() {
571: setJavaScriptHost(this, getIsolatedClassLoader());
572: }
573:
574: }
|