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