boot/src/main/java/org/netbeans/html/boot/impl/JavaScriptProcesor.java
author Jaroslav Tulach <jtulach@netbeans.org>
Tue, 26 Aug 2014 18:13:30 +0200
changeset 838 bdc3d696dd4a
parent 790 30f20d9c0986
child 911 3f5a6b1ac510
permissions -rw-r--r--
During the API review process (bug 246133) the reviewers decided that in order to include html4j to NetBeans Platform, we need to stop using org.apidesign namespace and switch to NetBeans one. Repackaging all SPI packages into org.netbeans.html.smthng.spi.
     1 /**
     2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3  *
     4  * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved.
     5  *
     6  * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
     7  * Other names may be trademarks of their respective owners.
     8  *
     9  * The contents of this file are subject to the terms of either the GNU
    10  * General Public License Version 2 only ("GPL") or the Common
    11  * Development and Distribution License("CDDL") (collectively, the
    12  * "License"). You may not use this file except in compliance with the
    13  * License. You can obtain a copy of the License at
    14  * http://www.netbeans.org/cddl-gplv2.html
    15  * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
    16  * specific language governing permissions and limitations under the
    17  * License.  When distributing the software, include this License Header
    18  * Notice in each file and include the License file at
    19  * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
    20  * particular file as subject to the "Classpath" exception as provided
    21  * by Oracle in the GPL Version 2 section of the License file that
    22  * accompanied this code. If applicable, add the following below the
    23  * License Header, with the fields enclosed by brackets [] replaced by
    24  * your own identifying information:
    25  * "Portions Copyrighted [year] [name of copyright owner]"
    26  *
    27  * Contributor(s):
    28  *
    29  * The Original Software is NetBeans. The Initial Developer of the Original
    30  * Software is Oracle. Portions Copyright 2013-2014 Oracle. All Rights Reserved.
    31  *
    32  * If you wish your version of this file to be governed by only the CDDL
    33  * or only the GPL Version 2, indicate your decision by adding
    34  * "[Contributor] elects to include this software in this distribution
    35  * under the [CDDL or GPL Version 2] license." If you do not indicate a
    36  * single choice of license, a recipient has the option to distribute
    37  * your version of this file under either the CDDL, the GPL Version 2 or
    38  * to extend the choice of license to its licensees as provided above.
    39  * However, if you add GPL Version 2 code and therefore, elected the GPL
    40  * Version 2 license, then the option applies only if the new code is
    41  * made subject to such option by the copyright holder.
    42  */
    43 package org.netbeans.html.boot.impl;
    44 
    45 import java.io.IOException;
    46 import java.io.OutputStream;
    47 import java.io.OutputStreamWriter;
    48 import java.io.PrintWriter;
    49 import java.io.Writer;
    50 import java.util.Collections;
    51 import java.util.HashMap;
    52 import java.util.HashSet;
    53 import java.util.List;
    54 import java.util.Map;
    55 import java.util.Set;
    56 import java.util.TreeMap;
    57 import javax.annotation.processing.AbstractProcessor;
    58 import javax.annotation.processing.Completion;
    59 import javax.annotation.processing.Completions;
    60 import javax.annotation.processing.Messager;
    61 import javax.annotation.processing.Processor;
    62 import javax.annotation.processing.RoundEnvironment;
    63 import javax.lang.model.SourceVersion;
    64 import javax.lang.model.element.AnnotationMirror;
    65 import javax.lang.model.element.Element;
    66 import javax.lang.model.element.ElementKind;
    67 import javax.lang.model.element.ExecutableElement;
    68 import javax.lang.model.element.Modifier;
    69 import javax.lang.model.element.Name;
    70 import javax.lang.model.element.PackageElement;
    71 import javax.lang.model.element.TypeElement;
    72 import javax.lang.model.element.VariableElement;
    73 import javax.lang.model.type.ArrayType;
    74 import javax.lang.model.type.ExecutableType;
    75 import javax.lang.model.type.TypeKind;
    76 import javax.lang.model.type.TypeMirror;
    77 import javax.tools.Diagnostic;
    78 import javax.tools.FileObject;
    79 import javax.tools.StandardLocation;
    80 import net.java.html.js.JavaScriptBody;
    81 import net.java.html.js.JavaScriptResource;
    82 import org.openide.util.lookup.ServiceProvider;
    83 
    84 /**
    85  *
    86  * @author Jaroslav Tulach
    87  */
    88 @ServiceProvider(service = Processor.class)
    89 public final class JavaScriptProcesor extends AbstractProcessor {
    90     private final Map<String,Map<String,ExecutableElement>> javacalls = 
    91         new HashMap<String,Map<String,ExecutableElement>>();
    92     private final Map<String,Set<TypeElement>> bodies = 
    93         new HashMap<String, Set<TypeElement>>();
    94     
    95     @Override
    96     public Set<String> getSupportedAnnotationTypes() {
    97         Set<String> set = new HashSet<String>();
    98         set.add(JavaScriptBody.class.getName());
    99         set.add(JavaScriptResource.class.getName());
   100         return set;
   101     }
   102     
   103     @Override
   104     public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   105         final Messager msg = processingEnv.getMessager();
   106         for (Element e : roundEnv.getElementsAnnotatedWith(JavaScriptBody.class)) {
   107             if (e.getKind() != ElementKind.METHOD && e.getKind() != ElementKind.CONSTRUCTOR) {
   108                 continue;
   109             }
   110             ExecutableElement ee = (ExecutableElement)e;
   111             List<? extends VariableElement> params = ee.getParameters();
   112             
   113             JavaScriptBody jsb = e.getAnnotation(JavaScriptBody.class);
   114             if (jsb == null) {
   115                 continue;
   116             } else {
   117                 Set<TypeElement> classes = this.bodies.get(findPkg(e));
   118                 if (classes == null) {
   119                     classes = new HashSet<TypeElement>();
   120                     bodies.put(findPkg(e), classes);
   121                 }
   122                 Element t = e.getEnclosingElement();
   123                 while (!t.getKind().isClass() && !t.getKind().isInterface()) {
   124                     t = t.getEnclosingElement();
   125                 }
   126                 classes.add((TypeElement)t);
   127             }
   128             String[] arr = jsb.args();
   129             if (params.size() != arr.length) {
   130                 msg.printMessage(Diagnostic.Kind.ERROR, "Number of args arguments does not match real arguments!", e);
   131             }
   132             if (!jsb.wait4js() && ee.getReturnType().getKind() != TypeKind.VOID) {
   133                 msg.printMessage(Diagnostic.Kind.ERROR, "Methods that don't wait for JavaScript to finish must return void!", e);
   134             }
   135             if (!jsb.javacall() && jsb.body().contains(".@")) {
   136                 msg.printMessage(Diagnostic.Kind.WARNING, "Usage of .@ usually requires javacall=true", e);
   137             }
   138             if (jsb.javacall()) {
   139                 JsCallback verify = new VerifyCallback(e);
   140                 try {
   141                     verify.parse(jsb.body());
   142                 } catch (IllegalStateException ex) {
   143                     msg.printMessage(Diagnostic.Kind.ERROR, ex.getLocalizedMessage(), e);
   144                 }
   145             }
   146         }
   147         for (Element e : roundEnv.getElementsAnnotatedWith(JavaScriptResource.class)) {
   148             JavaScriptResource r = e.getAnnotation(JavaScriptResource.class);
   149             if (r == null) {
   150                 continue;
   151             }
   152             final String res;
   153             if (r.value().startsWith("/")) {
   154                 res = r.value().substring(1);
   155             } else {
   156                 res = findPkg(e).replace('.', '/') + "/" + r.value();
   157             }
   158             
   159             try {
   160                 FileObject os = processingEnv.getFiler().getResource(StandardLocation.SOURCE_PATH, "", res);
   161                 os.openInputStream().close();
   162             } catch (IOException ex1) {
   163                 try {
   164                     FileObject os2 = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", res);
   165                     os2.openInputStream().close();
   166                 } catch (IOException ex2) {
   167                     try {
   168                         FileObject os3 = processingEnv.getFiler().getResource(StandardLocation.CLASS_PATH, "", res);
   169                         os3.openInputStream().close();
   170                     } catch (IOException ex3) {
   171                         msg.printMessage(Diagnostic.Kind.ERROR, "Cannot find resource " + res, e);
   172                     }
   173                 }
   174             }
   175             
   176             boolean found = false;
   177             for (Element mthod : e.getEnclosedElements()) {
   178                 if (mthod.getKind() != ElementKind.METHOD) {
   179                     continue;
   180                 }
   181                 if (mthod.getAnnotation(JavaScriptBody.class) != null) {
   182                     found = true;
   183                     break;
   184                 }
   185             }
   186             if (!found) {
   187                 msg.printMessage(Diagnostic.Kind.ERROR, "At least one method needs @JavaScriptBody annotation. "
   188                     + "Otherwise it is not guaranteed the resource will ever be loaded,", e
   189                 );
   190             }
   191         }
   192 
   193         if (roundEnv.processingOver()) {
   194             generateCallbackClass(javacalls);
   195             generateJavaScriptBodyList(bodies);
   196             javacalls.clear();
   197         }
   198         return true;
   199     }
   200 
   201     @Override
   202     public Iterable<? extends Completion> getCompletions(Element e, 
   203         AnnotationMirror annotation, ExecutableElement member, String userText
   204     ) {
   205         StringBuilder sb = new StringBuilder();
   206         if (e.getKind() == ElementKind.METHOD && member.getSimpleName().contentEquals("args")) {
   207             ExecutableElement ee = (ExecutableElement) e;
   208             String sep = "";
   209             sb.append("{ ");
   210             for (VariableElement ve : ee.getParameters()) {
   211                 sb.append(sep).append('"').append(ve.getSimpleName())
   212                     .append('"');
   213                 sep = ", ";
   214             }
   215             sb.append(" }");
   216             return Collections.nCopies(1, Completions.of(sb.toString()));
   217         }
   218         return null;
   219     }
   220 
   221     private class VerifyCallback extends JsCallback {
   222         private final Element e;
   223         public VerifyCallback(Element e) {
   224             this.e = e;
   225         }
   226 
   227         @Override
   228         protected CharSequence callMethod(String ident, String fqn, String method, String params) {
   229             final TypeElement type = processingEnv.getElementUtils().getTypeElement(fqn);
   230             if (type == null) {
   231                 processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
   232                     "Callback to non-existing class " + fqn, e
   233                 );
   234                 return "";
   235             }
   236             ExecutableElement found = null;
   237             StringBuilder foundParams = new StringBuilder();
   238             for (Element m : type.getEnclosedElements()) {
   239                 if (m.getKind() != ElementKind.METHOD) {
   240                     continue;
   241                 }
   242                 if (m.getSimpleName().contentEquals(method)) {
   243                     String paramTypes = findParamTypes((ExecutableElement)m);
   244                     if (paramTypes.equals(params)) {
   245                         found = (ExecutableElement) m;
   246                         break;
   247                     }
   248                     foundParams.append(paramTypes).append("\n");
   249                 }
   250             }
   251             if (found == null) {
   252                 if (foundParams.length() == 0) {
   253                     processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
   254                         "Callback to class " + fqn + " with unknown method " + method, e
   255                     );
   256                 } else {
   257                     processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
   258                         "Callback to " + fqn + "." + method + " with wrong parameters: " + 
   259                         params + ". Only known parameters are " + foundParams, e
   260                     );
   261                 }
   262             } else {
   263                 Map<String,ExecutableElement> mangledOnes = javacalls.get(findPkg(e));
   264                 if (mangledOnes == null) {
   265                     mangledOnes = new TreeMap<String, ExecutableElement>();
   266                     javacalls.put(findPkg(e), mangledOnes);
   267                 }
   268                 String mangled = JsCallback.mangle(fqn, method, findParamTypes(found));
   269                 mangledOnes.put(mangled, found);
   270             }
   271             return "";
   272         }
   273 
   274         private String findParamTypes(ExecutableElement method) {
   275             ExecutableType t = (ExecutableType) method.asType();
   276             StringBuilder sb = new StringBuilder();
   277             sb.append('(');
   278             for (TypeMirror tm : t.getParameterTypes()) {
   279                 if (tm.getKind().isPrimitive()) {
   280                     switch (tm.getKind()) {
   281                         case INT: sb.append('I'); break;
   282                         case BOOLEAN: sb.append('Z'); break;
   283                         case BYTE: sb.append('B'); break;
   284                         case CHAR: sb.append('C'); break;
   285                         case SHORT: sb.append('S'); break;
   286                         case DOUBLE: sb.append('D'); break;
   287                         case FLOAT: sb.append('F'); break;
   288                         case LONG: sb.append('J'); break;
   289                         default:
   290                             throw new IllegalStateException("Uknown " + tm.getKind());
   291                     }
   292                 } else {
   293                     while (tm.getKind() == TypeKind.ARRAY) {
   294                         sb.append('[');
   295                         tm = ((ArrayType)tm).getComponentType();
   296                     }
   297                     sb.append('L');
   298                     sb.append(tm.toString().replace('.', '/'));
   299                     sb.append(';');
   300                 }
   301             }
   302             sb.append(')');
   303             return sb.toString();
   304         }
   305     }
   306     
   307     private void generateJavaScriptBodyList(Map<String,Set<TypeElement>> bodies) {
   308         if (bodies.isEmpty()) {
   309             return;
   310         }
   311         try {
   312             FileObject all = processingEnv.getFiler().createResource(
   313                 StandardLocation.CLASS_OUTPUT, "", "META-INF/net.java.html.js.classes"                
   314             );
   315             PrintWriter wAll = new PrintWriter(new OutputStreamWriter(
   316                 all.openOutputStream(), "UTF-8"
   317             ));
   318             for (Map.Entry<String, Set<TypeElement>> entry : bodies.entrySet()) {
   319                 String pkg = entry.getKey();
   320                 Set<TypeElement> classes = entry.getValue();
   321 
   322                 FileObject out = processingEnv.getFiler().createResource(
   323                     StandardLocation.CLASS_OUTPUT, pkg, "net.java.html.js.classes",
   324                     classes.iterator().next()
   325                 );
   326                 OutputStream os = out.openOutputStream();
   327                 try {
   328                     PrintWriter w = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));
   329                     for (TypeElement type : classes) {
   330                         final Name bn = processingEnv.getElementUtils().getBinaryName(type);
   331                         w.println(bn);
   332                         wAll.println(bn);
   333                     }
   334                     w.flush();
   335                     w.close();
   336                 } catch (IOException x) {
   337                     processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write to " + entry.getKey() + ": " + x.toString());
   338                 } finally {
   339                     os.close();
   340                 }
   341             }
   342             wAll.close();
   343         } catch (IOException x) {
   344             processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write to " + "META-INF/net.java.html.js.classes: " + x.toString());
   345         }
   346     }
   347     
   348     private void generateCallbackClass(Map<String,Map<String, ExecutableElement>> process) {
   349         for (Map.Entry<String, Map<String, ExecutableElement>> pkgEn : process.entrySet()) {
   350             String pkgName = pkgEn.getKey();
   351             Map<String, ExecutableElement> map = pkgEn.getValue();
   352             StringBuilder source = new StringBuilder();
   353             source.append("package ").append(pkgName).append(";\n");
   354             source.append("public final class $JsCallbacks$ {\n");
   355             source.append("  static final $JsCallbacks$ VM = new $JsCallbacks$(null);\n");
   356             source.append("  private final org.netbeans.html.boot.spi.Fn.Presenter p;\n");
   357             source.append("  private $JsCallbacks$ last;\n");
   358             source.append("  private $JsCallbacks$(org.netbeans.html.boot.spi.Fn.Presenter p) {\n");
   359             source.append("    this.p = p;\n");
   360             source.append("  }\n");
   361             source.append("  final $JsCallbacks$ current() {\n");
   362             source.append("    org.netbeans.html.boot.spi.Fn.Presenter now = org.netbeans.html.boot.spi.Fn.activePresenter();\n");
   363             source.append("    if (now == p) return this;\n");
   364             source.append("    if (last != null && now == last.p) return last;\n");
   365             source.append("    return last = new $JsCallbacks$(now);\n");
   366             source.append("  }\n");
   367             for (Map.Entry<String, ExecutableElement> entry : map.entrySet()) {
   368                 final String mangled = entry.getKey();
   369                 final ExecutableElement m = entry.getValue();
   370                 final boolean isStatic = m.getModifiers().contains(Modifier.STATIC);
   371                 
   372                 source.append("\n  public java.lang.Object ")
   373                     .append(mangled)
   374                     .append("(");
   375                 
   376                 String sep = "";
   377                 if (!isStatic) {
   378                     source.append(((TypeElement)m.getEnclosingElement()).getQualifiedName());
   379                     source.append(" self");
   380                     sep = ", ";
   381                 }
   382                 
   383                 int cnt = 0;
   384                 StringBuilder convert = new StringBuilder();
   385                 for (VariableElement ve : m.getParameters()) {
   386                     source.append(sep);
   387                     ++cnt;
   388                     final TypeMirror t = ve.asType();
   389                     if (!t.getKind().isPrimitive()) {
   390                         source.append("Object");
   391                         convert.append("    if (p instanceof org.netbeans.html.boot.spi.Fn.FromJavaScript) {\n");
   392                         convert.append("      arg").append(cnt).
   393                             append(" = ((org.netbeans.html.boot.spi.Fn.FromJavaScript)p).toJava(arg").append(cnt).
   394                             append(");\n");
   395                         convert.append("    }\n");
   396                     } else {
   397                         source.append(t);
   398                     }
   399                     source.append(" arg").append(cnt);
   400                     sep = ", ";
   401                 }
   402                 source.append(") throws Throwable {\n");
   403                 source.append(convert);
   404                 if (useTryResources()) {
   405                     source.append("    try (java.io.Closeable a = org.netbeans.html.boot.spi.Fn.activate(p)) { \n");
   406                 } else {
   407                     source.append("    java.io.Closeable a = org.netbeans.html.boot.spi.Fn.activate(p); try {\n");
   408                 }
   409                 source.append("    ");
   410                 if (m.getReturnType().getKind() != TypeKind.VOID) {
   411                     source.append("Object $ret = ");
   412                 }
   413                 if (isStatic) {
   414                     source.append(((TypeElement)m.getEnclosingElement()).getQualifiedName());
   415                     source.append('.');
   416                 } else {
   417                     source.append("self.");
   418                 }
   419                 source.append(m.getSimpleName());
   420                 source.append("(");
   421                 cnt = 0;
   422                 sep = "";
   423                 for (VariableElement ve : m.getParameters()) {
   424                     source.append(sep);
   425                     source.append("(").append(ve.asType());
   426                     source.append(")arg").append(++cnt);
   427                     sep = ", ";
   428                 }
   429                 source.append(");\n");
   430                 if (m.getReturnType().getKind() == TypeKind.VOID) {
   431                     source.append("    return null;\n");
   432                 } else {
   433                     source.append("    if (p instanceof org.netbeans.html.boot.spi.Fn.ToJavaScript) {\n");
   434                     source.append("      $ret = ((org.netbeans.html.boot.spi.Fn.ToJavaScript)p).toJavaScript($ret);\n");
   435                     source.append("    }\n");
   436                     source.append("    return $ret;\n");
   437                 }
   438                 if (useTryResources()) {
   439                     source.append("    }\n");
   440                 } else {
   441                     
   442                     source.append("    } finally {\n");
   443                     source.append("      a.close();\n");
   444                     source.append("    }\n");
   445                 }
   446                 source.append("  }\n");
   447             }
   448             source.append("}\n");
   449             final String srcName = pkgName + ".$JsCallbacks$";
   450             try {
   451                 Writer w = processingEnv.getFiler().createSourceFile(srcName,
   452                     map.values().toArray(new Element[map.size()])
   453                 ).openWriter();
   454                 w.write(source.toString());
   455                 w.close();
   456             } catch (IOException ex) {
   457                 processingEnv.getMessager().printMessage(
   458                     Diagnostic.Kind.ERROR, "Can't write " + srcName + ": " + ex.getMessage()
   459                 );
   460             }
   461         }
   462     }
   463 
   464     private boolean useTryResources() {
   465         try {
   466             return processingEnv.getSourceVersion().compareTo(SourceVersion.RELEASE_7) >= 0;
   467         } catch (LinkageError err) {
   468             // can happen when running on JDK6
   469             return false;
   470         }
   471     }
   472     
   473     private static String findPkg(Element e) {
   474         while (e.getKind() != ElementKind.PACKAGE) {
   475             e = e.getEnclosingElement();
   476         }
   477         return ((PackageElement)e).getQualifiedName().toString();
   478     }
   479     
   480 }