You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

JsniBundleGenerator.java 10KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. /*
  2. * Copyright 2013, The gwtquery team.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.google.gwt.query.rebind;
  17. import com.google.gwt.core.ext.Generator;
  18. import com.google.gwt.core.ext.GeneratorContext;
  19. import com.google.gwt.core.ext.TreeLogger;
  20. import com.google.gwt.core.ext.UnableToCompleteException;
  21. import com.google.gwt.core.ext.typeinfo.JClassType;
  22. import com.google.gwt.core.ext.typeinfo.JMethod;
  23. import com.google.gwt.core.ext.typeinfo.TypeOracle;
  24. import com.google.gwt.query.client.builders.JsniBundle.LibrarySource;
  25. import com.google.gwt.query.client.builders.JsniBundle.MethodSource;
  26. import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
  27. import com.google.gwt.user.rebind.SourceWriter;
  28. import org.apache.commons.io.output.ByteArrayOutputStream;
  29. import java.io.IOException;
  30. import java.io.InputStream;
  31. import java.io.PrintWriter;
  32. import java.net.HttpURLConnection;
  33. import java.net.URL;
  34. import java.util.zip.GZIPInputStream;
  35. import java.util.zip.InflaterInputStream;
  36. /**
  37. * Generates an implementation of a user-defined interface <code>T</code> that
  38. * extends {@link JsniBundle}.
  39. *
  40. * The generated implementation includes hand-written external js-files into
  41. * jsni methods so as those files can take advantage of gwt compiler optimizations.
  42. *
  43. */
  44. public class JsniBundleGenerator extends Generator {
  45. public String generate(TreeLogger logger, GeneratorContext context, String requestedClass)
  46. throws UnableToCompleteException {
  47. TypeOracle oracle = context.getTypeOracle();
  48. JClassType clazz = oracle.findType(requestedClass);
  49. String packageName = clazz.getPackage().getName();
  50. String className = clazz.getName().replace('.', '_') + "_Impl";
  51. String fullName = packageName + "." + className;
  52. PrintWriter pw = context.tryCreate(logger, packageName, className);
  53. if (pw != null) {
  54. ClassSourceFileComposerFactory fact =
  55. new ClassSourceFileComposerFactory(packageName, className);
  56. if (clazz.isInterface() != null) {
  57. fact.addImplementedInterface(requestedClass);
  58. } else {
  59. fact.setSuperclass(requestedClass);
  60. }
  61. SourceWriter sw = fact.createSourceWriter(context, pw);
  62. if (sw != null) {
  63. for (JMethod method : clazz.getMethods()) {
  64. LibrarySource librarySource = method.getAnnotation(LibrarySource.class);
  65. String value, prepend, postpend;
  66. String replace[];
  67. if (librarySource != null) {
  68. value = librarySource.value();
  69. prepend = librarySource.prepend();
  70. postpend = librarySource.postpend();
  71. replace = librarySource.replace();
  72. } else {
  73. MethodSource methodSource = method.getAnnotation(MethodSource.class);
  74. if (methodSource != null) {
  75. value = methodSource.value();
  76. prepend = methodSource.prepend();
  77. postpend = methodSource.postpend();
  78. replace = methodSource.replace();
  79. } else {
  80. continue;
  81. }
  82. }
  83. try {
  84. // Read the javascript content
  85. String content = getContent(logger, packageName.replace(".", "/"), value);
  86. // Adjust javascript so as we can introduce it in a JSNI comment block without
  87. // breaking java syntax.
  88. String jsni = parseJavascriptSource(content);
  89. for (int i = 0; i < replace.length - 1; i += 2) {
  90. jsni = jsni.replaceAll(replace[i], replace[i + 1]);
  91. }
  92. pw.println(method.toString().replace("abstract", "native") + "/*-{");
  93. pw.println(prepend);
  94. pw.println(jsni);
  95. pw.println(postpend);
  96. pw.println("}-*/;");
  97. } catch (Exception e) {
  98. logger.log(TreeLogger.ERROR, "Error parsing javascript source: " + value + " "
  99. + e.getMessage());
  100. throw new UnableToCompleteException();
  101. }
  102. }
  103. }
  104. sw.commit(logger);
  105. }
  106. return fullName;
  107. }
  108. /**
  109. * Get the content of a javascript source. It supports remote sources hosted in CDN's.
  110. */
  111. private String getContent(TreeLogger logger, String path, String src)
  112. throws UnableToCompleteException {
  113. HttpURLConnection connection = null;
  114. InputStream in = null;
  115. try {
  116. if (!src.matches("(?i)https?://.*")) {
  117. String file = path + "/" + src;
  118. in = this.getClass().getClassLoader().getResourceAsStream(file);
  119. if (in == null) {
  120. // If we didn't find the resource relative to the package, assume it is absolute.
  121. file = src;
  122. in = this.getClass().getClassLoader().getResourceAsStream(file);
  123. }
  124. if (in != null) {
  125. logger.log(TreeLogger.INFO, getClass().getSimpleName()
  126. + " - importing external javascript: " + file);
  127. } else {
  128. logger.log(TreeLogger.ERROR, "Unable to read javascript file: " + file);
  129. }
  130. } else {
  131. logger.log(TreeLogger.INFO, getClass().getSimpleName()
  132. + " - downloading external javascript: " + src);
  133. URL url = new URL(src);
  134. connection = (HttpURLConnection) url.openConnection();
  135. connection.setRequestProperty("Accept-Encoding", "gzip, deflate");
  136. connection.setRequestProperty("Host", url.getHost());
  137. connection.setConnectTimeout(3000);
  138. connection.setReadTimeout(3000);
  139. int status = connection.getResponseCode();
  140. if (status != HttpURLConnection.HTTP_OK) {
  141. logger.log(TreeLogger.ERROR, "Server Error: " + status + " "
  142. + connection.getResponseMessage());
  143. throw new UnableToCompleteException();
  144. }
  145. String encoding = connection.getContentEncoding();
  146. in = connection.getInputStream();
  147. if ("gzip".equalsIgnoreCase(encoding)) {
  148. in = new GZIPInputStream(in);
  149. } else if ("deflate".equalsIgnoreCase(encoding)) {
  150. in = new InflaterInputStream(in);
  151. }
  152. }
  153. return inputStreamToString(in);
  154. } catch (IOException e) {
  155. logger.log(TreeLogger.ERROR, "Error: " + e.getMessage());
  156. throw new UnableToCompleteException();
  157. } finally {
  158. if (connection != null) {
  159. connection.disconnect();
  160. }
  161. }
  162. }
  163. /**
  164. * Adapt a java-script block which could produce a syntax error when
  165. * embedding it in a JSNI block.
  166. *
  167. * The objective is to replace any 'c' comment-ending occurrence to avoid closing
  168. * JSNI comment blocks prematurely.
  169. *
  170. * A Regexp based parser is not reliable, this approach is better and faster.
  171. */
  172. // Note: this comment is intentionally using c++ style to allow writing the '*/' sequence.
  173. //
  174. // - Remove C comments: /* ... */
  175. // - Remove C++ comments: // ...
  176. // - Escape certain strings: '...*/...' to '...*' + '/...'
  177. // - Rewrite inline regex: /...*/igm to new RegExp('...*' + 'igm');
  178. private String parseJavascriptSource(String js) throws Exception {
  179. boolean isJS = true;
  180. boolean isSingQuot = false;
  181. boolean isDblQuot = false;
  182. boolean isSlash = false;
  183. boolean isCComment = false;
  184. boolean isCPPComment = false;
  185. boolean isRegex = false;
  186. boolean isOper = false;
  187. StringBuilder ret = new StringBuilder();
  188. String tmp = "";
  189. Character last = 0;
  190. Character prev = 0;
  191. for (int i = 0, l = js.length(); i < l; i++) {
  192. Character c = js.charAt(i);
  193. String out = c.toString();
  194. if (isJS) {
  195. isDblQuot = c == '"';
  196. isSingQuot = c == '\'';
  197. isSlash = c == '/';
  198. isJS = !isDblQuot && !isSingQuot && !isSlash;
  199. if (!isJS) {
  200. out = tmp = "";
  201. isCPPComment = isCComment = isRegex = false;
  202. }
  203. } else if (isSingQuot) {
  204. isJS = !(isSingQuot = last == '\\' || c != '\'');
  205. if (isJS)
  206. out = escapeQuotedString(tmp, c);
  207. else
  208. tmp += c;
  209. } else if (isDblQuot) {
  210. isJS = !(isDblQuot = last == '\\' || c != '"');
  211. if (isJS)
  212. out = escapeQuotedString(tmp, c);
  213. else
  214. tmp += c;
  215. } else if (isSlash) {
  216. if (!isCPPComment && !isCComment && !isRegex && !isOper) {
  217. isCPPComment = c == '/';
  218. isCComment = c == '*';
  219. isOper = !isCPPComment && !isCComment && !"=(&|?:;},".contains("" + prev);
  220. isRegex = !isCPPComment && !isCComment && !isOper;
  221. }
  222. if (isOper) {
  223. isJS = !(isSlash = isOper = false);
  224. out = "" + last + c;
  225. } else if (isCPPComment) {
  226. isJS = !(isSlash = isCPPComment = c != '\n');
  227. if (isJS)
  228. out = "\n";
  229. } else if (isCComment) {
  230. isSlash = isCComment = !(isJS = last == '*' && c == '/');
  231. if (isJS)
  232. out = "";
  233. } else if (isRegex) {
  234. isJS = !(isSlash = isRegex = last == '\\' || c != '/');
  235. if (isJS) {
  236. String mod = "";
  237. while (++i < l) {
  238. c = js.charAt(i);
  239. if ("igm".contains("" + c))
  240. mod += c;
  241. else
  242. break;
  243. }
  244. out = escapeInlineRegex(tmp, mod) + c;
  245. } else {
  246. tmp += c;
  247. }
  248. } else {
  249. isJS = true;
  250. }
  251. }
  252. if (isJS) {
  253. ret.append(out);
  254. }
  255. if (last != ' ') {
  256. prev = last;
  257. }
  258. last = prev == '\\' && c == '\\' ? 0 : c;
  259. }
  260. return ret.toString();
  261. }
  262. private String escapeQuotedString(String s, Character quote) {
  263. return quote + s.replace("*/", "*" + quote + " + " + quote + "/") + quote;
  264. }
  265. private String escapeInlineRegex(String s, String mod) {
  266. if (s.endsWith("*")) {
  267. return "new RegExp('" + s.replace("\\", "\\\\") + "','" + mod + "')";
  268. } else {
  269. return '/' + s + '/' + mod;
  270. }
  271. }
  272. private String inputStreamToString(InputStream in) throws IOException {
  273. ByteArrayOutputStream bytes = new ByteArrayOutputStream();
  274. byte[] buffer = new byte[4096];
  275. int read = in.read(buffer);
  276. while (read != -1) {
  277. bytes.write(buffer, 0, read);
  278. read = in.read(buffer);
  279. }
  280. in.close();
  281. return bytes.toString();
  282. }
  283. }