boot/src/main/java/org/netbeans/html/boot/impl/JavaScriptProcesor.java
author Jaroslav Tulach <jtulach@netbeans.org>
Sat, 02 Aug 2014 12:59:31 +0200
changeset 790 30f20d9c0986
parent 676 c2d1bf0e7edf
child 838 bdc3d696dd4a
permissions -rw-r--r--
Fixing Javadoc to succeed on JDK8
     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.apidesign.html.boot.spi.Fn.Presenter p;\n");
   357             source.append("  private $JsCallbacks$ last;\n");
   358             source.append("  private $JsCallbacks$(org.apidesign.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.apidesign.html.boot.spi.Fn.Presenter now = org.apidesign.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.apidesign.html.boot.spi.Fn.FromJavaScript) {\n");
   392                         convert.append("      arg").append(cnt).
   393                             append(" = ((org.apidesign.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.apidesign.html.boot.spi.Fn.activate(p)) { \n");
   406                 } else {
   407                     source.append("    java.io.Closeable a = org.apidesign.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.apidesign.html.boot.spi.Fn.ToJavaScript) {\n");
   434                     source.append("      $ret = ((org.apidesign.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 }