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.util;
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.JMethod;
021: import com.google.gwt.core.ext.typeinfo.JParameter;
022: import com.google.gwt.core.ext.typeinfo.JType;
023: import com.google.gwt.dev.js.JsParser;
024: import com.google.gwt.dev.js.JsParserException;
025: import com.google.gwt.dev.js.JsSourceGenerationVisitor;
026: import com.google.gwt.dev.js.JsParserException.SourceDetail;
027: import com.google.gwt.dev.js.ast.JsBlock;
028: import com.google.gwt.dev.js.ast.JsContext;
029: import com.google.gwt.dev.js.ast.JsExprStmt;
030: import com.google.gwt.dev.js.ast.JsExpression;
031: import com.google.gwt.dev.js.ast.JsFunction;
032: import com.google.gwt.dev.js.ast.JsNameRef;
033: import com.google.gwt.dev.js.ast.JsNode;
034: import com.google.gwt.dev.js.ast.JsProgram;
035: import com.google.gwt.dev.js.ast.JsStatement;
036: import com.google.gwt.dev.shell.JavaScriptHost;
037:
038: import java.io.IOException;
039: import java.io.StringReader;
040: import java.util.List;
041:
042: /**
043: * Helper methods working with JSNI.
044: */
045: public class Jsni {
046:
047: /**
048: * Represents a logical interval of text.
049: */
050: public static class Interval {
051:
052: public final int end;
053:
054: public final int start;
055:
056: public Interval(int start, int end) {
057: this .start = start;
058: this .end = end;
059: }
060: }
061:
062: /**
063: * Generate source code, fixing up any JSNI references for hosted mode.
064: *
065: * <p/><table>
066: * <tr>
067: * <td>Original</td>
068: * <td>Becomes</td>
069: * </tr>
070: * <tr>
071: * <td><code>.@class::method(params)(args)</code></td>
072: *
073: * <td><code>["@class::method(params)"](args)</code></td>
074: * </tr>
075: * <tr>
076: * <td><code>@class::method(params)(args)</code></td>
077: *
078: * <td><code>__static["@class::method(params)"](args)</code></td>
079: * </tr>
080: * <tr>
081: * <td><code>.@class::field</code></td>
082: *
083: * <td><code>["@class::field"]</code></td>
084: * </tr>
085: * <tr>
086: * <td><code>@class::field</code></td>
087: *
088: * <td><code>__static["@class::field"]</code></td>
089: * </tr>
090: * </table>
091: */
092: private static class JsSourceGenWithJsniIdentFixup extends
093: JsSourceGenerationVisitor {
094: private final TextOutput out;
095:
096: public JsSourceGenWithJsniIdentFixup(TextOutput out) {
097: super (out);
098: this .out = out;
099: }
100:
101: @Override
102: public boolean visit(JsNameRef x, JsContext<JsExpression> ctx) {
103: String ident = x.getIdent();
104: if (ident.startsWith("@")) {
105: JsExpression q = x.getQualifier();
106: if (q != null) {
107: accept(q);
108: out.print("[\"");
109: out.print(ident);
110: out.print("\"]");
111: } else {
112: out.print("__static[\"");
113: out.print(ident);
114: out.print("\"]");
115: }
116: return false;
117: } else {
118: return super .visit(x, ctx);
119: }
120: }
121: }
122:
123: public static final String JAVASCRIPTHOST_NAME = JavaScriptHost.class
124: .getName();
125:
126: public static final String JSNI_BLOCK_END = "}-*/";
127:
128: public static final String JSNI_BLOCK_START = "/*-{";
129:
130: /**
131: * Generates the code to wrap a set of parameters as an object array.
132: */
133: public static String buildArgList(JMethod method) {
134: StringBuffer sb = new StringBuffer();
135: sb.append("new Object[]{");
136:
137: JParameter[] params = method.getParameters();
138: for (int i = 0; i < params.length; ++i) {
139: if (i > 0) {
140: sb.append(", ");
141: }
142:
143: JType type = params[i].getType();
144: String typeName = type.getQualifiedSourceName();
145:
146: if ((type.isArray() == null)
147: && (type.isPrimitive() != null)) {
148: // Primitive types have to be wrapped for reflection invoke().
149: //
150: if (typeName.equals("boolean")) {
151: sb.append("new Boolean(" + params[i].getName()
152: + ")");
153: } else if (typeName.equals("byte")) {
154: sb.append("new Byte(" + params[i].getName() + ")");
155: } else if (typeName.equals("char")) {
156: sb.append("new Character(" + params[i].getName()
157: + ")");
158: } else if (typeName.equals("short")) {
159: sb.append("new Short(" + params[i].getName() + ")");
160: } else if (typeName.equals("int")) {
161: sb.append("new Integer(" + params[i].getName()
162: + ")");
163: } else if (typeName.equals("float")) {
164: sb.append("new Float(" + params[i].getName() + ")");
165: } else if (typeName.equals("double")) {
166: sb
167: .append("new Double(" + params[i].getName()
168: + ")");
169: } else if (typeName.equals("long")) {
170: sb.append("new Long(" + params[i].getName() + ")");
171: } else {
172: throw new RuntimeException(
173: "Unexpected primitive parameter type");
174: }
175: } else {
176: // Reference types pass through as themselves.
177: //
178: sb.append(params[i].getName());
179: }
180: }
181:
182: sb.append("}");
183: String args = sb.toString();
184: return args;
185: }
186:
187: /**
188: * Generates the code to pass the exact types associated with each argument of
189: * this method.
190: */
191: public static String buildTypeList(JMethod method) {
192: StringBuffer sb = new StringBuffer();
193: sb.append("new Class[]{");
194:
195: JParameter[] params = method.getParameters();
196: for (int i = 0; i < params.length; ++i) {
197: if (i > 0) {
198: sb.append(", ");
199: }
200:
201: JType type = params[i].getType();
202: String typeName = type.getErasedType()
203: .getQualifiedSourceName();
204: sb.append(typeName);
205: sb.append(".class");
206: }
207:
208: sb.append("}");
209: String classes = sb.toString();
210: return classes;
211: }
212:
213: public static int countNewlines(char[] buf, int start, int end) {
214: int total = 0;
215: while (start < end) {
216: switch (buf[start]) {
217: case '\r':
218: ++total;
219: // if the next character is a linefeed, eat it too
220: if (start + 1 < end && buf[start + 1] == '\n') {
221: ++start;
222: }
223: break;
224: case '\n':
225: ++total;
226: break;
227: }
228: ++start;
229: }
230: return total;
231: }
232:
233: public static int countNewlines(String src, int start, int end) {
234: return countNewlines(src.toCharArray(), start, end);
235: }
236:
237: /**
238: * Replaces double-quotes and backslashes in native JS code with their
239: * appropriate escaped form (so they can be encoded in a java string).
240: */
241: public static String escapeQuotesAndSlashes(String str) {
242: StringBuffer buf = new StringBuffer(str);
243: escapeQuotesAndSlashes(buf);
244: return buf.toString();
245: }
246:
247: public static Interval findJsniSource(JMethod method)
248: throws UnableToCompleteException {
249: assert (method.isNative());
250: int bodyStart = method.getBodyStart();
251: int bodyEnd = method.getBodyEnd();
252: int bodyLen = bodyEnd - bodyStart + 1;
253: char[] source = method.getEnclosingType().getCompilationUnit()
254: .getSource();
255: String js = String.valueOf(source, bodyStart, bodyLen);
256:
257: int jsniStart = js.indexOf(JSNI_BLOCK_START);
258: if (jsniStart == -1) {
259: return null;
260: }
261:
262: int jsniEnd = js.indexOf(JSNI_BLOCK_END, jsniStart);
263: if (jsniEnd == -1) {
264: // Suspicious, but maybe this is just a weird comment, so let it slide.
265: //
266: return null;
267: }
268:
269: int srcStart = bodyStart + jsniStart
270: + JSNI_BLOCK_START.length();
271: int srcEnd = bodyStart + jsniEnd;
272: return new Interval(srcStart, srcEnd);
273: }
274:
275: /**
276: * Returns a string representing the source output of the JsNode, where all
277: * JSNI idents have been replaced with legal JavaScript for hosted mode.
278: *
279: * The output has quotes and slashes escaped so that the result can be part of
280: * a legal Java source code string literal.
281: */
282: public static String generateEscapedJavaScriptForHostedMode(
283: JsNode<?> node) {
284: String source = generateJavaScriptForHostedMode(node);
285: StringBuffer body = new StringBuffer(source.length());
286: body.append(source);
287: escapeQuotesAndSlashes(body);
288: fixupLinebreaks(body);
289: return body.toString();
290: }
291:
292: /**
293: * Gets a unique name for this method and its signature (this is used to
294: * determine whether one method overrides another).
295: */
296: public static String getJsniSignature(JMethod method) {
297: String name = method.getName();
298: String className = method.getEnclosingType()
299: .getQualifiedSourceName();
300:
301: StringBuffer sb = new StringBuffer();
302: sb.append(className);
303: sb.append("::");
304: sb.append(name);
305: sb.append("(");
306: JParameter[] params = method.getParameters();
307: for (int i = 0; i < params.length; ++i) {
308: JParameter param = params[i];
309: String typeSig = param.getType().getJNISignature();
310: sb.append(typeSig);
311: }
312: sb.append(")");
313: String fullName = sb.toString();
314: return fullName;
315: }
316:
317: /**
318: * In other words, it can have <code>return</code> statements.
319: */
320: public static JsBlock parseAsFunctionBody(TreeLogger logger,
321: String js, String location, int startLine)
322: throws UnableToCompleteException {
323: // Wrap it in fake function and parse it.
324: js = "function(){ " + js + " }";
325:
326: JsParser jsParser = new JsParser();
327: JsProgram jsPgm = new JsProgram();
328: StringReader r = new StringReader(js);
329:
330: try {
331: List<JsStatement> stmts = jsParser.parse(jsPgm.getScope(),
332: r, startLine);
333:
334: // Rip the body out of the parsed function and attach the JavaScript
335: // AST to the method.
336: //
337: JsFunction fn = (JsFunction) ((JsExprStmt) stmts.get(0))
338: .getExpression();
339: return fn.getBody();
340: } catch (IOException e) {
341: logger.log(TreeLogger.ERROR,
342: "Error reading JavaScript source", e);
343: throw new UnableToCompleteException();
344: } catch (JsParserException e) {
345: SourceDetail dtl = e.getSourceDetail();
346: if (dtl != null) {
347: StringBuffer sb = new StringBuffer();
348: sb.append(location);
349: sb.append("(");
350: sb.append(dtl.getLine());
351: sb.append(", ");
352: sb.append(dtl.getLineOffset());
353: sb.append("): ");
354: sb.append(e.getMessage());
355: logger.log(TreeLogger.ERROR, sb.toString(), e);
356: throw new UnableToCompleteException();
357: } else {
358: logger.log(TreeLogger.ERROR,
359: "Error parsing JSNI source", e);
360: throw new UnableToCompleteException();
361: }
362: }
363: }
364:
365: /**
366: * Replaces double-quotes and backslashes in native JS code with their
367: * appropriate escaped form (so they can be encoded in a java string).
368: */
369: private static void escapeQuotesAndSlashes(StringBuffer buf) {
370: for (int i = 0; i < buf.length(); ++i) {
371: char c = buf.charAt(i);
372: if (c == '\"' || c == '\\') {
373: buf.insert(i, '\\');
374: i += 1;
375: }
376: }
377: }
378:
379: /**
380: * Replaces any actual carriage returns and linebreaks we put in earlier with
381: * an escaped form (so they can be encoded in a java string).
382: */
383: private static void fixupLinebreaks(StringBuffer body) {
384: for (int i = 0; i < body.length(); ++i) {
385: char c = body.charAt(i);
386: if (c == '\r') {
387: body.setCharAt(i, 'r');
388: body.insert(i, '\\');
389: i += 1;
390: } else if (c == '\n') {
391: body.setCharAt(i, 'n');
392: body.insert(i, '\\');
393: i += 1;
394: }
395: }
396: }
397:
398: /**
399: * Returns a string representing the source output of the JsNode, where all
400: * JSNI idents have been replaced with legal JavaScript for hosted mode.
401: */
402: private static String generateJavaScriptForHostedMode(JsNode<?> node) {
403: DefaultTextOutput out = new DefaultTextOutput(false);
404: JsSourceGenWithJsniIdentFixup vi = new JsSourceGenWithJsniIdentFixup(
405: out);
406: vi.accept(node);
407: return out.toString();
408: }
409:
410: }
|