boot/src/main/java/org/netbeans/html/boot/impl/JavaScriptProcesor.java
author Jaroslav Tulach <jtulach@netbeans.org>
Fri, 15 Jan 2016 13:05:42 +0100
changeset 1043 b189d001b9bd
parent 1042 e633fed12064
permissions -rw-r--r--
#257579: Erase the parameter types before computing the signature
     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.Arrays;
    51 import java.util.Collections;
    52 import java.util.HashMap;
    53 import java.util.HashSet;
    54 import java.util.List;
    55 import java.util.Map;
    56 import java.util.Set;
    57 import java.util.TreeMap;
    58 import javax.annotation.processing.AbstractProcessor;
    59 import javax.annotation.processing.Completion;
    60 import javax.annotation.processing.Completions;
    61 import javax.annotation.processing.Messager;
    62 import javax.annotation.processing.Processor;
    63 import javax.annotation.processing.RoundEnvironment;
    64 import javax.lang.model.SourceVersion;
    65 import javax.lang.model.element.AnnotationMirror;
    66 import javax.lang.model.element.Element;
    67 import javax.lang.model.element.ElementKind;
    68 import javax.lang.model.element.ExecutableElement;
    69 import javax.lang.model.element.Modifier;
    70 import javax.lang.model.element.Name;
    71 import javax.lang.model.element.PackageElement;
    72 import javax.lang.model.element.TypeElement;
    73 import javax.lang.model.element.VariableElement;
    74 import javax.lang.model.type.ArrayType;
    75 import javax.lang.model.type.ExecutableType;
    76 import javax.lang.model.type.TypeKind;
    77 import javax.lang.model.type.TypeMirror;
    78 import javax.lang.model.util.Types;
    79 import javax.tools.Diagnostic;
    80 import javax.tools.FileObject;
    81 import javax.tools.StandardLocation;
    82 import net.java.html.js.JavaScriptBody;
    83 import net.java.html.js.JavaScriptResource;
    84 import org.openide.util.lookup.ServiceProvider;
    85 
    86 /**
    87  *
    88  * @author Jaroslav Tulach
    89  */
    90 @ServiceProvider(service = Processor.class)
    91 public final class JavaScriptProcesor extends AbstractProcessor {
    92     private final Map<String,Map<String,ExecutableElement>> javacalls =
    93         new HashMap<String,Map<String,ExecutableElement>>();
    94     private final Map<String,Set<TypeElement>> bodies =
    95         new HashMap<String, Set<TypeElement>>();
    96 
    97     @Override
    98     public Set<String> getSupportedAnnotationTypes() {
    99         Set<String> set = new HashSet<String>();
   100         set.add(JavaScriptBody.class.getName());
   101         set.add(JavaScriptResource.class.getName());
   102         return set;
   103     }
   104 
   105     @Override
   106     public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
   107         final Messager msg = processingEnv.getMessager();
   108         for (Element e : roundEnv.getElementsAnnotatedWith(JavaScriptBody.class)) {
   109             if (e.getKind() != ElementKind.METHOD && e.getKind() != ElementKind.CONSTRUCTOR) {
   110                 continue;
   111             }
   112             ExecutableElement ee = (ExecutableElement)e;
   113             List<? extends VariableElement> params = ee.getParameters();
   114 
   115             JavaScriptBody jsb = e.getAnnotation(JavaScriptBody.class);
   116             if (jsb == null) {
   117                 continue;
   118             } else {
   119                 Set<TypeElement> classes = this.bodies.get(findPkg(e));
   120                 if (classes == null) {
   121                     classes = new HashSet<TypeElement>();
   122                     bodies.put(findPkg(e), classes);
   123                 }
   124                 Element t = e.getEnclosingElement();
   125                 while (!t.getKind().isClass() && !t.getKind().isInterface()) {
   126                     t = t.getEnclosingElement();
   127                 }
   128                 classes.add((TypeElement)t);
   129             }
   130             String[] arr = jsb.args();
   131             if (params.size() != arr.length) {
   132                 msg.printMessage(Diagnostic.Kind.ERROR, "Number of args arguments does not match real arguments!", e);
   133             }
   134             for (int i = 0; i < arr.length; i++) {
   135                 if (!params.get(i).getSimpleName().toString().equals(arr[i])) {
   136                     msg.printMessage(Diagnostic.Kind.WARNING, "Actual method parameter names and args ones " + Arrays.toString(arr) + " differ", e);
   137                 }
   138             }
   139             if (!jsb.wait4js() && ee.getReturnType().getKind() != TypeKind.VOID) {
   140                 msg.printMessage(Diagnostic.Kind.ERROR, "Methods that don't wait for JavaScript to finish must return void!", e);
   141             }
   142             if (!jsb.javacall() && jsb.body().contains(".@")) {
   143                 msg.printMessage(Diagnostic.Kind.WARNING, "Usage of .@ usually requires javacall=true", e);
   144             }
   145             if (jsb.javacall()) {
   146                 JsCallback verify = new VerifyCallback(e);
   147                 try {
   148                     verify.parse(jsb.body());
   149                 } catch (IllegalStateException ex) {
   150                     msg.printMessage(Diagnostic.Kind.ERROR, ex.getLocalizedMessage(), e);
   151                 }
   152             }
   153         }
   154         for (Element e : roundEnv.getElementsAnnotatedWith(JavaScriptResource.class)) {
   155             JavaScriptResource r = e.getAnnotation(JavaScriptResource.class);
   156             if (r == null) {
   157                 continue;
   158             }
   159             final String res;
   160             if (r.value().startsWith("/")) {
   161                 res = r.value().substring(1);
   162             } else {
   163                 res = findPkg(e).replace('.', '/') + "/" + r.value();
   164             }
   165 
   166             try {
   167                 FileObject os = processingEnv.getFiler().getResource(StandardLocation.SOURCE_PATH, "", res);
   168                 os.openInputStream().close();
   169             } catch (IOException ex1) {
   170                 try {
   171                     FileObject os2 = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", res);
   172                     os2.openInputStream().close();
   173                 } catch (IOException ex2) {
   174                     try {
   175                         FileObject os3 = processingEnv.getFiler().getResource(StandardLocation.CLASS_PATH, "", res);
   176                         os3.openInputStream().close();
   177                     } catch (IOException ex3) {
   178                         msg.printMessage(Diagnostic.Kind.ERROR, "Cannot find resource " + res, e);
   179                     }
   180                 }
   181             }
   182 
   183             boolean found = false;
   184             for (Element mthod : e.getEnclosedElements()) {
   185                 if (mthod.getKind() != ElementKind.METHOD) {
   186                     continue;
   187                 }
   188                 if (mthod.getAnnotation(JavaScriptBody.class) != null) {
   189                     found = true;
   190                     break;
   191                 }
   192             }
   193             if (!found) {
   194                 msg.printMessage(Diagnostic.Kind.ERROR, "At least one method needs @JavaScriptBody annotation. "
   195                     + "Otherwise it is not guaranteed the resource will ever be loaded,", e
   196                 );
   197             }
   198         }
   199 
   200         if (roundEnv.processingOver()) {
   201             generateCallbackClass(javacalls);
   202             generateJavaScriptBodyList(bodies);
   203             javacalls.clear();
   204         }
   205         return true;
   206     }
   207 
   208     @Override
   209     public Iterable<? extends Completion> getCompletions(Element e,
   210         AnnotationMirror annotation, ExecutableElement member, String userText
   211     ) {
   212         StringBuilder sb = new StringBuilder();
   213         if (e.getKind() == ElementKind.METHOD && member.getSimpleName().contentEquals("args")) {
   214             ExecutableElement ee = (ExecutableElement) e;
   215             String sep = "";
   216             sb.append("{ ");
   217             for (VariableElement ve : ee.getParameters()) {
   218                 sb.append(sep).append('"').append(ve.getSimpleName())
   219                     .append('"');
   220                 sep = ", ";
   221             }
   222             sb.append(" }");
   223             return Collections.nCopies(1, Completions.of(sb.toString()));
   224         }
   225         return null;
   226     }
   227 
   228     private class VerifyCallback extends JsCallback {
   229         private final Element e;
   230         public VerifyCallback(Element e) {
   231             this.e = e;
   232         }
   233 
   234         @Override
   235         protected CharSequence callMethod(String ident, String fqn, String method, String params) {
   236             final TypeElement type = processingEnv.getElementUtils().getTypeElement(fqn);
   237             if (type == null) {
   238                 processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
   239                     "Callback to non-existing class " + fqn, e
   240                 );
   241                 return "";
   242             }
   243             ExecutableElement found = null;
   244             StringBuilder foundParams = new StringBuilder();
   245             for (Element m : type.getEnclosedElements()) {
   246                 if (m.getKind() != ElementKind.METHOD) {
   247                     continue;
   248                 }
   249                 if (m.getSimpleName().contentEquals(method)) {
   250                     String paramTypes = findParamTypes((ExecutableElement)m);
   251                     if (paramTypes.equals(params)) {
   252                         found = (ExecutableElement) m;
   253                         break;
   254                     }
   255                     foundParams.append(paramTypes).append("\n");
   256                 }
   257             }
   258             if (found == null) {
   259                 if (foundParams.length() == 0) {
   260                     processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
   261                         "Callback to class " + fqn + " with unknown method " + method, e
   262                     );
   263                 } else {
   264                     processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
   265                         "Callback to " + fqn + "." + method + " with wrong parameters: " +
   266                         params + ". Only known parameters are " + foundParams, e
   267                     );
   268                 }
   269             } else {
   270                 Map<String,ExecutableElement> mangledOnes = javacalls.get(findPkg(e));
   271                 if (mangledOnes == null) {
   272                     mangledOnes = new TreeMap<String, ExecutableElement>();
   273                     javacalls.put(findPkg(e), mangledOnes);
   274                 }
   275                 String mangled = JsCallback.mangle(fqn, method, findParamTypes(found));
   276                 mangledOnes.put(mangled, found);
   277             }
   278             return "";
   279         }
   280 
   281         private String findParamTypes(ExecutableElement method) {
   282             ExecutableType t = (ExecutableType) method.asType();
   283             StringBuilder sb = new StringBuilder();
   284             sb.append('(');
   285             for (TypeMirror tm : t.getParameterTypes()) {
   286                 if (tm.getKind().isPrimitive()) {
   287                     switch (tm.getKind()) {
   288                         case INT: sb.append('I'); break;
   289                         case BOOLEAN: sb.append('Z'); break;
   290                         case BYTE: sb.append('B'); break;
   291                         case CHAR: sb.append('C'); break;
   292                         case SHORT: sb.append('S'); break;
   293                         case DOUBLE: sb.append('D'); break;
   294                         case FLOAT: sb.append('F'); break;
   295                         case LONG: sb.append('J'); break;
   296                         default:
   297                             throw new IllegalStateException("Uknown " + tm.getKind());
   298                     }
   299                 } else {
   300                     while (tm.getKind() == TypeKind.ARRAY) {
   301                         sb.append('[');
   302                         tm = ((ArrayType)tm).getComponentType();
   303                     }
   304                     sb.append('L');
   305                     Types tu = processingEnv.getTypeUtils();
   306                     Element elm = tu.asElement(tu.erasure(tm));
   307                     dumpElems(sb, elm, ';');
   308                 }
   309             }
   310             sb.append(')');
   311             return sb.toString();
   312         }
   313     }
   314 
   315     private static void dumpElems(StringBuilder sb, Element e, char after) {
   316         if (e == null) {
   317             return;
   318         }
   319         if (e.getKind() == ElementKind.PACKAGE) {
   320             PackageElement pe = (PackageElement) e;
   321             sb.append(pe.getQualifiedName().toString().replace('.', '/')).append('/');
   322             return;
   323         }
   324         Element p = e.getEnclosingElement();
   325         dumpElems(sb, p, '$');
   326         sb.append(e.getSimpleName());
   327         sb.append(after);
   328     }
   329 
   330     private void generateJavaScriptBodyList(Map<String,Set<TypeElement>> bodies) {
   331         if (bodies.isEmpty()) {
   332             return;
   333         }
   334         try {
   335             FileObject all = processingEnv.getFiler().createResource(
   336                 StandardLocation.CLASS_OUTPUT, "", "META-INF/net.java.html.js.classes"
   337             );
   338             PrintWriter wAll = new PrintWriter(new OutputStreamWriter(
   339                 all.openOutputStream(), "UTF-8"
   340             ));
   341             for (Map.Entry<String, Set<TypeElement>> entry : bodies.entrySet()) {
   342                 String pkg = entry.getKey();
   343                 Set<TypeElement> classes = entry.getValue();
   344 
   345                 FileObject out = processingEnv.getFiler().createResource(
   346                     StandardLocation.CLASS_OUTPUT, pkg, "net.java.html.js.classes",
   347                     classes.iterator().next()
   348                 );
   349                 OutputStream os = out.openOutputStream();
   350                 try {
   351                     PrintWriter w = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));
   352                     for (TypeElement type : classes) {
   353                         final Name bn = processingEnv.getElementUtils().getBinaryName(type);
   354                         w.println(bn);
   355                         wAll.println(bn);
   356                     }
   357                     w.flush();
   358                     w.close();
   359                 } catch (IOException x) {
   360                     processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write to " + entry.getKey() + ": " + x.toString());
   361                 } finally {
   362                     os.close();
   363                 }
   364             }
   365             wAll.close();
   366         } catch (IOException x) {
   367             processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write to " + "META-INF/net.java.html.js.classes: " + x.toString());
   368         }
   369     }
   370 
   371     private void generateCallbackClass(Map<String,Map<String, ExecutableElement>> process) {
   372         for (Map.Entry<String, Map<String, ExecutableElement>> pkgEn : process.entrySet()) {
   373             String pkgName = pkgEn.getKey();
   374             Map<String, ExecutableElement> map = pkgEn.getValue();
   375             StringBuilder source = new StringBuilder();
   376             source.append("package ").append(pkgName).append(";\n");
   377             source.append("public final class $JsCallbacks$ {\n");
   378             source.append("  static final $JsCallbacks$ VM = new $JsCallbacks$(null);\n");
   379             source.append("  private final org.netbeans.html.boot.spi.Fn.Presenter p;\n");
   380             source.append("  private $JsCallbacks$ last;\n");
   381             source.append("  private $JsCallbacks$(org.netbeans.html.boot.spi.Fn.Presenter p) {\n");
   382             source.append("    this.p = p;\n");
   383             source.append("  }\n");
   384             source.append("  final $JsCallbacks$ current() {\n");
   385             source.append("    org.netbeans.html.boot.spi.Fn.Presenter now = org.netbeans.html.boot.spi.Fn.activePresenter();\n");
   386             source.append("    if (now == p) return this;\n");
   387             source.append("    if (last != null && now == last.p) return last;\n");
   388             source.append("    return last = new $JsCallbacks$(now);\n");
   389             source.append("  }\n");
   390             for (Map.Entry<String, ExecutableElement> entry : map.entrySet()) {
   391                 final String mangled = entry.getKey();
   392                 final ExecutableElement m = entry.getValue();
   393                 generateMethod(false, m, source, mangled);
   394                 generateMethod(true, m, source, "raw$" + mangled);
   395             }
   396             source.append("}\n");
   397             final String srcName = pkgName + ".$JsCallbacks$";
   398             try {
   399                 Writer w = processingEnv.getFiler().createSourceFile(srcName,
   400                     map.values().toArray(new Element[map.size()])
   401                 ).openWriter();
   402                 w.write(source.toString());
   403                 w.close();
   404             } catch (IOException ex) {
   405                 processingEnv.getMessager().printMessage(
   406                     Diagnostic.Kind.ERROR, "Can't write " + srcName + ": " + ex.getMessage()
   407                 );
   408             }
   409         }
   410     }
   411 
   412     private void generateMethod(boolean selfObj, final ExecutableElement m, StringBuilder source, final String mangled) {
   413         final boolean isStatic = m.getModifiers().contains(Modifier.STATIC);
   414         if (isStatic && selfObj) {
   415             return;
   416         }
   417         final TypeElement selfType = (TypeElement)m.getEnclosingElement();
   418         Types tu = processingEnv.getTypeUtils();
   419 
   420         source.append("\n  public java.lang.Object ")
   421                 .append(mangled)
   422                 .append("(");
   423 
   424         String sep = "";
   425         StringBuilder convert = new StringBuilder();
   426         if (!isStatic) {
   427             if (selfObj) {
   428                 source.append("java.lang.Object self");
   429                 convert.append("    if (p instanceof org.netbeans.html.boot.spi.Fn.FromJavaScript) {\n");
   430                 convert.append("      self").
   431                         append(" = ((org.netbeans.html.boot.spi.Fn.FromJavaScript)p).toJava(self").
   432                         append(");\n");
   433                 convert.append("    }\n");
   434             } else {
   435                 source.append(selfType.getQualifiedName());
   436                 source.append(" self");
   437             }
   438             sep = ", ";
   439         }
   440 
   441         int cnt = 0;
   442         for (VariableElement ve : m.getParameters()) {
   443             source.append(sep);
   444             ++cnt;
   445             final TypeMirror t = ve.asType();
   446             if (!t.getKind().isPrimitive() && !"java.lang.String".equals(t.toString())) { // NOI18N
   447                 source.append("java.lang.Object");
   448                 convert.append("    if (p instanceof org.netbeans.html.boot.spi.Fn.FromJavaScript) {\n");
   449                 convert.append("      arg").append(cnt).
   450                         append(" = ((org.netbeans.html.boot.spi.Fn.FromJavaScript)p).toJava(arg").append(cnt).
   451                         append(");\n");
   452                 convert.append("    }\n");
   453             } else {
   454                 source.append(t);
   455             }
   456             source.append(" arg").append(cnt);
   457             sep = ", ";
   458         }
   459         source.append(") throws Throwable {\n");
   460         source.append(convert);
   461         if (useTryResources()) {
   462             source.append("    try (java.io.Closeable a = org.netbeans.html.boot.spi.Fn.activate(p)) { \n");
   463         } else {
   464             source.append("    java.io.Closeable a = org.netbeans.html.boot.spi.Fn.activate(p); try {\n");
   465         }
   466         source.append("    ");
   467         if (m.getReturnType().getKind() != TypeKind.VOID) {
   468             source.append("java.lang.Object $ret = ");
   469         }
   470         if (isStatic) {
   471             source.append(((TypeElement)m.getEnclosingElement()).getQualifiedName());
   472             source.append('.');
   473         } else {
   474             if (selfObj) {
   475                 source.append("((");
   476                 source.append(selfType.getQualifiedName());
   477                 source.append(")self).");
   478             } else {
   479                 source.append("self.");
   480             }
   481         }
   482         source.append(m.getSimpleName());
   483         source.append("(");
   484         cnt = 0;
   485         sep = "";
   486         for (VariableElement ve : m.getParameters()) {
   487             source.append(sep);
   488             source.append("(").append(tu.erasure(ve.asType()));
   489             source.append(")arg").append(++cnt);
   490             sep = ", ";
   491         }
   492         source.append(");\n");
   493         if (m.getReturnType().getKind() == TypeKind.VOID) {
   494             source.append("    return null;\n");
   495         } else {
   496             source.append("    if (p instanceof org.netbeans.html.boot.spi.Fn.ToJavaScript) {\n");
   497             source.append("      $ret = ((org.netbeans.html.boot.spi.Fn.ToJavaScript)p).toJavaScript($ret);\n");
   498             source.append("    }\n");
   499             source.append("    return $ret;\n");
   500         }
   501         if (useTryResources()) {
   502             source.append("    }\n");
   503         } else {
   504 
   505             source.append("    } finally {\n");
   506             source.append("      a.close();\n");
   507             source.append("    }\n");
   508         }
   509         source.append("  }\n");
   510     }
   511 
   512     private boolean useTryResources() {
   513         try {
   514             return processingEnv.getSourceVersion().compareTo(SourceVersion.RELEASE_7) >= 0;
   515         } catch (LinkageError err) {
   516             // can happen when running on JDK6
   517             return false;
   518         }
   519     }
   520 
   521     private static String findPkg(Element e) {
   522         while (e.getKind() != ElementKind.PACKAGE) {
   523             e = e.getEnclosingElement();
   524         }
   525         return ((PackageElement)e).getQualifiedName().toString();
   526     }
   527 
   528 }