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: import com.google.gwt.core.ext.typeinfo.CompilationUnitProvider;
021: import com.google.gwt.core.ext.typeinfo.JClassType;
022: import com.google.gwt.core.ext.typeinfo.JMethod;
023: import com.google.gwt.core.ext.typeinfo.JParameter;
024: import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
025: import com.google.gwt.core.ext.typeinfo.JType;
026: import com.google.gwt.core.ext.typeinfo.NotFoundException;
027: import com.google.gwt.core.ext.typeinfo.TypeOracle;
028: import com.google.gwt.dev.jdt.CompilationUnitProviderWithAlternateSource;
029: import com.google.gwt.dev.js.ast.JsBlock;
030: import com.google.gwt.dev.util.Jsni;
031: import com.google.gwt.dev.util.StringCopier;
032: import com.google.gwt.dev.util.Util;
033:
034: import java.io.File;
035: import java.util.ArrayList;
036: import java.util.Arrays;
037: import java.util.IdentityHashMap;
038: import java.util.List;
039: import java.util.Map;
040:
041: /**
042: * Adapts compilation units containing JSNI-accessible code by rewriting the
043: * source.
044: */
045: public class JsniInjector {
046:
047: /**
048: * A consolidated way to get all expected types and succeed or fail
049: * atomically.
050: */
051: private class CoreTypes {
052: static final String CLS_JSOBJECT = "JavaScriptObject";
053: static final String CLS_STRING = "String";
054: static final String PKG_JSOBJECT = "com.google.gwt.core.client";
055: static final String PKG_STRING = "java.lang";
056:
057: public final JClassType javaLangString;
058:
059: public final JClassType javaScriptObject;
060:
061: public CoreTypes(TreeLogger logger)
062: throws UnableToCompleteException {
063: javaScriptObject = getCoreType(logger, PKG_JSOBJECT,
064: CLS_JSOBJECT);
065: javaLangString = getCoreType(logger, PKG_STRING, CLS_STRING);
066: }
067:
068: private JClassType getCoreType(TreeLogger logger, String pkg,
069: String cls) throws UnableToCompleteException {
070: try {
071: return oracle.getType(pkg, cls);
072: } catch (NotFoundException e) {
073: String msg = "Unable to find core type '" + pkg + "."
074: + cls + "'";
075: logger.log(TreeLogger.ERROR, msg, e);
076: throw new UnableToCompleteException();
077: }
078: }
079: }
080:
081: /**
082: * A chunk of replacement text and where to put it.
083: */
084: private static class Replacement implements Comparable<Replacement> {
085: public final int end;
086:
087: public final int start;
088:
089: public final char[] text;
090:
091: public Replacement(int start, int end, char[] text) {
092: this .start = start;
093: this .end = end;
094: this .text = text;
095: }
096:
097: public int compareTo(Replacement other) {
098: if (start < other.start) {
099: assert (end <= other.start) : "Overlapping changes not supported";
100: return -1;
101: } else if (start > other.start) {
102: assert (start >= other.end) : "Overlapping changes not supported";
103: return 1;
104: } else {
105: return 0;
106: }
107: }
108: }
109:
110: private CoreTypes coreTypes;
111:
112: private final TypeOracle oracle;
113:
114: private final Map<JMethod, JsBlock> parsedJsByMethod = new IdentityHashMap<JMethod, JsBlock>();
115:
116: public JsniInjector(TypeOracle oracle) {
117: this .oracle = oracle;
118: }
119:
120: public CompilationUnitProvider inject(TreeLogger logger,
121: CompilationUnitProvider cup, File jsniSaveDirectory)
122: throws UnableToCompleteException {
123:
124: logger = logger.branch(TreeLogger.SPAM,
125: "Checking for JavaScript native methods", null);
126:
127: // Make sure the core types exist.
128: if (coreTypes == null) {
129: coreTypes = new CoreTypes(logger);
130: }
131:
132: // Analyze the source and build a list of changes.
133: char[] source = cup.getSource();
134: List<Replacement> changes = new ArrayList<Replacement>();
135: rewriteCompilationUnit(logger, source, changes, cup, false);
136:
137: // Sort and apply the changes.
138: int n = changes.size();
139: if (n > 0) {
140: Replacement[] repls = changes.toArray(new Replacement[n]);
141: Arrays.sort(repls);
142: StringCopier copier = new StringCopier(source);
143: for (int i = 0; i < n; ++i) {
144: Replacement repl = repls[i];
145: copier.commit(repl.text, repl.start, repl.end);
146: }
147:
148: char[] results = copier.finish();
149:
150: if (jsniSaveDirectory != null) {
151: String originalPath = cup.getLocation().replace(
152: File.separatorChar, '/');
153: String suffix = cup.getPackageName().replace('.', '/');
154: int pos = originalPath.indexOf(suffix);
155: if (pos >= 0) {
156: String filePath = originalPath.substring(pos);
157: File out = new File(jsniSaveDirectory, filePath);
158: Util.writeCharsAsFile(logger, out, results);
159: }
160: }
161:
162: return new CompilationUnitProviderWithAlternateSource(cup,
163: results);
164: } else {
165: // No changes were made, so we return the original.
166: logger.log(TreeLogger.SPAM,
167: "No JavaScript native methods were found", null);
168: return cup;
169: }
170: }
171:
172: /**
173: * Static initialization: generate one call to 'JavaScriptHost.createNative()'
174: * for each native method, to define the JavaScript code that will be invoked
175: * later.
176: */
177: private char[] genInitializerBlock(String file, char[] source,
178: JMethod[] methods) {
179:
180: String escapedFile = Jsni.escapeQuotesAndSlashes(file);
181:
182: StringBuffer sb = new StringBuffer();
183: sb.append(" static {");
184: for (int i = 0; i < methods.length; ++i) {
185: JMethod method = methods[i];
186:
187: JsBlock jsniBody = parsedJsByMethod.get(method);
188: if (jsniBody == null) {
189: // Not a JSNI method.
190: //
191: continue;
192: }
193:
194: JParameter[] params = method.getParameters();
195: String paramNamesArray = getParamNamesArrayExpr(params);
196:
197: final String jsTry = "try ";
198: final String jsCatch = " catch (e) {\\n"
199: + " __static[\\\"@"
200: + Jsni.JAVASCRIPTHOST_NAME
201: + "::exceptionCaught"
202: + "(ILjava/lang/String;Ljava/lang/String;)\\\"]"
203: + "((e && e.number) || 0, (e && e.name) || null , (e && e.message) || null);\\n"
204: + "}\\n";
205:
206: /*
207: * Surround the original JS body statements with a try/catch so that we
208: * can map JavaScript exceptions back into Java. Note that the method body
209: * itself will print curly braces, so we don't need them around the
210: * try/catch.
211: */
212: String js = jsTry
213: + Jsni
214: .generateEscapedJavaScriptForHostedMode(jsniBody)
215: + jsCatch;
216: String jsniSig = Jsni.getJsniSignature(method);
217:
218: // figure out starting line number
219: int bodyStart = method.getBodyStart();
220: int line = Jsni.countNewlines(source, 0, bodyStart) + 1;
221:
222: sb.append(" " + Jsni.JAVASCRIPTHOST_NAME
223: + ".createNative(\"" + escapedFile + "\", " + line
224: + ", " + "\"@" + jsniSig + "\", " + paramNamesArray
225: + ", \"" + js + "\");");
226: }
227: sb.append("}");
228: return sb.toString().toCharArray();
229: }
230:
231: /**
232: * Create a legal Java method call that will result in a JSNI invocation.
233: *
234: * @param method
235: * @param expectedHeaderLines
236: * @param expectedBodyLines
237: * @param prettyPrint true if the output should be prettier
238: * @return a String of the Java code to call a JSNI method, using
239: * JavaScriptHost.invokeNative*
240: */
241: private String genNonNativeVersionOfJsniMethod(JMethod method,
242: int expectedHeaderLines, int expectedBodyLines,
243: boolean pretty) {
244: StringBuffer sb = new StringBuffer();
245: String nl = pretty ? "\n " : "";
246:
247: // Add extra lines at the start to match comments + declaration
248: if (!pretty) {
249: for (int i = 0; i < expectedHeaderLines; ++i) {
250: sb.append('\n');
251: }
252: }
253:
254: String methodDecl = method.getReadableDeclaration(false, true,
255: false, false, false);
256:
257: sb.append(methodDecl + " {" + nl);
258: // wrap the call in a try-catch block
259: sb.append("try {" + nl);
260:
261: // Write the Java call to the property invoke method, adding
262: // downcasts where necessary.
263: JType returnType = method.getReturnType();
264: boolean isJavaScriptObject = isJavaScriptObject(returnType);
265: JPrimitiveType primType;
266: if (isJavaScriptObject) {
267: // Add a downcast from Handle to the originally-declared type.
268: String returnTypeName = returnType
269: .getParameterizedQualifiedSourceName();
270: sb.append("return (" + returnTypeName + ")"
271: + Jsni.JAVASCRIPTHOST_NAME + ".invokeNativeHandle");
272: } else if (null != (primType = returnType.isPrimitive())) {
273: // Primitives have special overloads.
274: char[] primTypeSuffix = primType.getSimpleSourceName()
275: .toCharArray();
276: primTypeSuffix[0] = Character
277: .toUpperCase(primTypeSuffix[0]);
278: String invokeMethodName = "invokeNative"
279: + String.valueOf(primTypeSuffix);
280: if (primType != JPrimitiveType.VOID) {
281: sb.append("return ");
282: }
283: sb.append(Jsni.JAVASCRIPTHOST_NAME);
284: sb.append(".");
285: sb.append(invokeMethodName);
286: } else if (returnType == coreTypes.javaLangString) {
287: sb.append("return ");
288: sb.append(Jsni.JAVASCRIPTHOST_NAME);
289: sb.append(".invokeNativeString");
290: } else {
291: // Some reference type.
292: // We need to add a downcast to the originally-declared type.
293: String returnTypeName = returnType
294: .getParameterizedQualifiedSourceName();
295: sb.append("return (");
296: sb.append(returnTypeName);
297: sb.append(")");
298: sb.append(Jsni.JAVASCRIPTHOST_NAME);
299: sb.append(".invokeNativeObject");
300: }
301:
302: // Write the argument list for the invoke call.
303: sb.append("(\"@");
304: String jsniSig = Jsni.getJsniSignature(method);
305: sb.append(jsniSig);
306: if (method.isStatic()) {
307: sb.append("\", null, ");
308: } else {
309: sb.append("\", this, ");
310: }
311:
312: if (isJavaScriptObject) {
313: // Handle-oriented calls also need the return type as an argument.
314: String returnTypeName = returnType.getErasedType()
315: .getQualifiedSourceName();
316: sb.append(returnTypeName);
317: sb.append(".class, ");
318: }
319:
320: // Build an array of classes that tells the invoker how to adapt the
321: // incoming arguments for calling into JavaScript.
322: sb.append(Jsni.buildTypeList(method));
323: sb.append(',');
324:
325: // Build an array containing the arguments based on the names of the
326: // parameters.
327: sb.append(Jsni.buildArgList(method));
328: sb.append(");" + nl);
329:
330: // Catch exceptions; rethrow if the exception is RTE or declared.
331: sb.append("} catch (java.lang.Throwable __gwt_exception) {"
332: + nl);
333: sb
334: .append("if (__gwt_exception instanceof java.lang.RuntimeException) throw (java.lang.RuntimeException) __gwt_exception;"
335: + nl);
336: JType[] throwTypes = method.getThrows();
337: for (int i = 0; i < throwTypes.length; ++i) {
338: String typeName = throwTypes[i].getQualifiedSourceName();
339: sb.append("if (__gwt_exception instanceof " + typeName
340: + ") throw (" + typeName + ") __gwt_exception;"
341: + nl);
342: }
343: sb
344: .append("throw new java.lang.RuntimeException(\"Undeclared checked exception thrown out of JavaScript; web mode behavior may differ.\", __gwt_exception);"
345: + nl);
346: sb.append("}" + nl);
347:
348: sb.append("}" + nl);
349:
350: // Add extra lines at the end to match JSNI body.
351: if (!pretty) {
352: for (int i = 0; i < expectedBodyLines; ++i) {
353: sb.append('\n');
354: }
355: }
356:
357: return sb.toString();
358: }
359:
360: private String getParamNamesArrayExpr(JParameter[] params) {
361: StringBuffer sb = new StringBuffer();
362: sb.append("new String[] {");
363: for (int i = 0, n = params.length; i < n; ++i) {
364: if (i > 0) {
365: sb.append(", ");
366: }
367:
368: JParameter param = params[i];
369: sb.append('\"');
370: sb.append(param.getName());
371: sb.append('\"');
372: }
373: sb.append("}");
374: return sb.toString();
375: }
376:
377: private boolean isJavaScriptObject(JType type) {
378: JClassType classType = type.isClass();
379: if (classType == null) {
380: return false;
381: }
382:
383: if (classType.isAssignableTo(coreTypes.javaScriptObject)) {
384: return true;
385: } else {
386: return false;
387: }
388: }
389:
390: private void rewriteCompilationUnit(TreeLogger logger,
391: char[] source, List<Replacement> changes,
392: CompilationUnitProvider cup, boolean pretty)
393: throws UnableToCompleteException {
394:
395: // Hit all the types in the compilation unit.
396: JClassType[] types = oracle.getTypesInCompilationUnit(cup);
397: for (int i = 0; i < types.length; i++) {
398: JClassType type = types[i];
399: rewriteType(logger, source, changes, type, pretty);
400: }
401: }
402:
403: private void rewriteType(TreeLogger logger, char[] source,
404: List<Replacement> changes, JClassType type, boolean pretty)
405: throws UnableToCompleteException {
406:
407: String loc = type.getCompilationUnit().getLocation();
408:
409: // Examine each method for JSNIness.
410: List<JMethod> patchedMethods = new ArrayList<JMethod>();
411: JMethod[] methods = type.getMethods();
412: for (int i = 0; i < methods.length; i++) {
413: JMethod method = methods[i];
414: if (method.isNative()) {
415: Jsni.Interval interval = Jsni.findJsniSource(method);
416: if (interval != null) {
417: // The method itself needs to be replaced.
418:
419: // Parse it.
420: String js = String.valueOf(source, interval.start,
421: interval.end - interval.start);
422: int startLine = Jsni.countNewlines(source, 0,
423: interval.start) + 1;
424: JsBlock body = Jsni.parseAsFunctionBody(logger, js,
425: loc, startLine);
426:
427: // Remember this as being a valid JSNI method.
428: parsedJsByMethod.put(method, body);
429:
430: // Replace the method.
431: final int declStart = method.getDeclStart();
432: final int declEnd = method.getDeclEnd();
433:
434: int expectedHeaderLines = Jsni.countNewlines(
435: source, declStart, interval.start);
436: int expectedBodyLines = Jsni.countNewlines(source,
437: interval.start, interval.end);
438: String newDecl = genNonNativeVersionOfJsniMethod(
439: method, expectedHeaderLines,
440: expectedBodyLines, pretty);
441:
442: final char[] newSource = newDecl.toCharArray();
443: changes.add(new Replacement(declStart, declEnd,
444: newSource));
445: patchedMethods.add(method);
446: } else {
447: // report error
448: String msg = "No JavaScript body found for native method '"
449: + method + "' in type '" + type + "'";
450: logger.log(TreeLogger.ERROR, msg, null);
451: throw new UnableToCompleteException();
452: }
453: }
454: }
455:
456: if (!patchedMethods.isEmpty()) {
457: JMethod[] patched = new JMethod[patchedMethods.size()];
458: patched = patchedMethods.toArray(patched);
459:
460: TreeLogger branch = logger
461: .branch(TreeLogger.SPAM, "Patched methods in '"
462: + type.getQualifiedSourceName() + "'", null);
463:
464: for (int i = 0; i < patched.length; i++) {
465: branch.log(TreeLogger.SPAM, patched[i]
466: .getReadableDeclaration(), null);
467: }
468:
469: // Insert an initializer block immediately after the opening brace of the
470: // class.
471: char[] block = genInitializerBlock(loc, source, patched);
472:
473: // If this is a non-static inner class, actually put the initializer block
474: // in the first enclosing static or top-level class instead.
475: while (type.getEnclosingType() != null && !type.isStatic()) {
476: type = type.getEnclosingType();
477: }
478:
479: int bodyStart = type.getBodyStart();
480: changes.add(new Replacement(bodyStart, bodyStart, block));
481: }
482: }
483: }
|