boot/src/main/java/org/netbeans/html/boot/impl/JavaScriptProcesor.java
author Jaroslav Tulach <jaroslav.tulach@netbeans.org>
Fri, 07 Feb 2014 07:44:34 +0100
changeset 551 7ca2253fa86d
parent 531 ff48a4a875ff
child 570 0e831501e072
permissions -rw-r--r--
Updating copyright headers to mention current year
     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 <jtulach@netbeans.org>
    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.javacall() && jsb.body().contains(".@")) {
   133                 msg.printMessage(Diagnostic.Kind.WARNING, "Usage of .@ usually requires javacall=true", e);
   134             }
   135             if (jsb.javacall()) {
   136                 JsCallback verify = new VerifyCallback(e);
   137                 try {
   138                     verify.parse(jsb.body());
   139                 } catch (IllegalStateException ex) {
   140                     msg.printMessage(Diagnostic.Kind.ERROR, ex.getLocalizedMessage(), e);
   141                 }
   142             }
   143         }
   144         for (Element e : roundEnv.getElementsAnnotatedWith(JavaScriptResource.class)) {
   145             JavaScriptResource r = e.getAnnotation(JavaScriptResource.class);
   146             if (r == null) {
   147                 continue;
   148             }
   149             final String res;
   150             if (r.value().startsWith("/")) {
   151                 res = r.value();
   152             } else {
   153                 res = findPkg(e).replace('.', '/') + "/" + r.value();
   154             }
   155             
   156             try {
   157                 FileObject os = processingEnv.getFiler().getResource(StandardLocation.SOURCE_PATH, "", res);
   158                 os.openInputStream().close();
   159             } catch (IOException ex1) {
   160                 try {
   161                     FileObject os2 = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", res);
   162                     os2.openInputStream().close();
   163                 } catch (IOException ex2) {
   164                     msg.printMessage(Diagnostic.Kind.ERROR, "Cannot find " + res + " in " + res + " package", e);
   165                 }
   166             }
   167             
   168             boolean found = false;
   169             for (Element mthod : e.getEnclosedElements()) {
   170                 if (mthod.getKind() != ElementKind.METHOD) {
   171                     continue;
   172                 }
   173                 if (mthod.getAnnotation(JavaScriptBody.class) != null) {
   174                     found = true;
   175                     break;
   176                 }
   177             }
   178             if (!found) {
   179                 msg.printMessage(Diagnostic.Kind.ERROR, "At least one method needs @JavaScriptBody annotation. "
   180                     + "Otherwise it is not guaranteed the resource will ever be loaded,", e
   181                 );
   182             }
   183         }
   184 
   185         if (roundEnv.processingOver()) {
   186             generateCallbackClass(javacalls);
   187             generateJavaScriptBodyList(bodies);
   188             javacalls.clear();
   189         }
   190         return true;
   191     }
   192 
   193     @Override
   194     public Iterable<? extends Completion> getCompletions(Element e, 
   195         AnnotationMirror annotation, ExecutableElement member, String userText
   196     ) {
   197         StringBuilder sb = new StringBuilder();
   198         if (e.getKind() == ElementKind.METHOD && member.getSimpleName().contentEquals("args")) {
   199             ExecutableElement ee = (ExecutableElement) e;
   200             String sep = "";
   201             sb.append("{ ");
   202             for (VariableElement ve : ee.getParameters()) {
   203                 sb.append(sep).append('"').append(ve.getSimpleName())
   204                     .append('"');
   205                 sep = ", ";
   206             }
   207             sb.append(" }");
   208             return Collections.nCopies(1, Completions.of(sb.toString()));
   209         }
   210         return null;
   211     }
   212 
   213     private class VerifyCallback extends JsCallback {
   214         private final Element e;
   215         public VerifyCallback(Element e) {
   216             this.e = e;
   217         }
   218 
   219         @Override
   220         protected CharSequence callMethod(String ident, String fqn, String method, String params) {
   221             final TypeElement type = processingEnv.getElementUtils().getTypeElement(fqn);
   222             if (type == null) {
   223                 processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
   224                     "Callback to non-existing class " + fqn, e
   225                 );
   226                 return "";
   227             }
   228             ExecutableElement found = null;
   229             StringBuilder foundParams = new StringBuilder();
   230             for (Element m : type.getEnclosedElements()) {
   231                 if (m.getKind() != ElementKind.METHOD) {
   232                     continue;
   233                 }
   234                 if (m.getSimpleName().contentEquals(method)) {
   235                     String paramTypes = findParamTypes((ExecutableElement)m);
   236                     if (paramTypes.equals(params)) {
   237                         found = (ExecutableElement) m;
   238                         break;
   239                     }
   240                     foundParams.append(paramTypes).append("\n");
   241                 }
   242             }
   243             if (found == null) {
   244                 if (foundParams.length() == 0) {
   245                     processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
   246                         "Callback to class " + fqn + " with unknown method " + method, e
   247                     );
   248                 } else {
   249                     processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
   250                         "Callback to " + fqn + "." + method + " with wrong parameters: " + 
   251                         params + ". Only known parameters are " + foundParams, e
   252                     );
   253                 }
   254             } else {
   255                 Map<String,ExecutableElement> mangledOnes = javacalls.get(findPkg(e));
   256                 if (mangledOnes == null) {
   257                     mangledOnes = new TreeMap<String, ExecutableElement>();
   258                     javacalls.put(findPkg(e), mangledOnes);
   259                 }
   260                 String mangled = JsCallback.mangle(fqn, method, findParamTypes(found));
   261                 mangledOnes.put(mangled, found);
   262             }
   263             return "";
   264         }
   265 
   266         private String findParamTypes(ExecutableElement method) {
   267             ExecutableType t = (ExecutableType) method.asType();
   268             StringBuilder sb = new StringBuilder();
   269             sb.append('(');
   270             for (TypeMirror tm : t.getParameterTypes()) {
   271                 if (tm.getKind().isPrimitive()) {
   272                     switch (tm.getKind()) {
   273                         case INT: sb.append('I'); break;
   274                         case BOOLEAN: sb.append('Z'); break;
   275                         case BYTE: sb.append('B'); break;
   276                         case CHAR: sb.append('C'); break;
   277                         case SHORT: sb.append('S'); break;
   278                         case DOUBLE: sb.append('D'); break;
   279                         case FLOAT: sb.append('F'); break;
   280                         case LONG: sb.append('J'); break;
   281                         default:
   282                             throw new IllegalStateException("Uknown " + tm.getKind());
   283                     }
   284                 } else {
   285                     while (tm.getKind() == TypeKind.ARRAY) {
   286                         sb.append('[');
   287                         tm = ((ArrayType)tm).getComponentType();
   288                     }
   289                     sb.append('L');
   290                     sb.append(tm.toString().replace('.', '/'));
   291                     sb.append(';');
   292                 }
   293             }
   294             sb.append(')');
   295             return sb.toString();
   296         }
   297     }
   298     
   299     private void generateJavaScriptBodyList(Map<String,Set<TypeElement>> bodies) {
   300         if (bodies.isEmpty()) {
   301             return;
   302         }
   303         try {
   304             FileObject all = processingEnv.getFiler().createResource(
   305                 StandardLocation.CLASS_OUTPUT, "", "META-INF/net.java.html.js.classes"                
   306             );
   307             PrintWriter wAll = new PrintWriter(new OutputStreamWriter(
   308                 all.openOutputStream(), "UTF-8"
   309             ));
   310             for (Map.Entry<String, Set<TypeElement>> entry : bodies.entrySet()) {
   311                 String pkg = entry.getKey();
   312                 Set<TypeElement> classes = entry.getValue();
   313 
   314                 FileObject out = processingEnv.getFiler().createResource(
   315                     StandardLocation.CLASS_OUTPUT, pkg, "net.java.html.js.classes",
   316                     classes.iterator().next()
   317                 );
   318                 OutputStream os = out.openOutputStream();
   319                 try {
   320                     PrintWriter w = new PrintWriter(new OutputStreamWriter(os, "UTF-8"));
   321                     for (TypeElement type : classes) {
   322                         final Name bn = processingEnv.getElementUtils().getBinaryName(type);
   323                         w.println(bn);
   324                         wAll.println(bn);
   325                     }
   326                     w.flush();
   327                     w.close();
   328                 } catch (IOException x) {
   329                     processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write to " + entry.getKey() + ": " + x.toString());
   330                 } finally {
   331                     os.close();
   332                 }
   333             }
   334             wAll.close();
   335         } catch (IOException x) {
   336             processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write to " + "META-INF/net.java.html.js.classes: " + x.toString());
   337         }
   338     }
   339     
   340     private void generateCallbackClass(Map<String,Map<String, ExecutableElement>> process) {
   341         for (Map.Entry<String, Map<String, ExecutableElement>> pkgEn : process.entrySet()) {
   342             String pkgName = pkgEn.getKey();
   343             Map<String, ExecutableElement> map = pkgEn.getValue();
   344             StringBuilder source = new StringBuilder();
   345             source.append("package ").append(pkgName).append(";\n");
   346             source.append("public final class $JsCallbacks$ {\n");
   347             source.append("  static final $JsCallbacks$ VM = new $JsCallbacks$(null);\n");
   348             source.append("  private final org.apidesign.html.boot.spi.Fn.Presenter p;\n");
   349             source.append("  private $JsCallbacks$ last;\n");
   350             source.append("  private $JsCallbacks$(org.apidesign.html.boot.spi.Fn.Presenter p) {\n");
   351             source.append("    this.p = p;\n");
   352             source.append("  }\n");
   353             source.append("  final $JsCallbacks$ current() {\n");
   354             source.append("    org.apidesign.html.boot.spi.Fn.Presenter now = org.apidesign.html.boot.spi.Fn.activePresenter();\n");
   355             source.append("    if (now == p) return this;\n");
   356             source.append("    if (last != null && now == last.p) return last;\n");
   357             source.append("    return last = new $JsCallbacks$(now);\n");
   358             source.append("  }\n");
   359             for (Map.Entry<String, ExecutableElement> entry : map.entrySet()) {
   360                 final String mangled = entry.getKey();
   361                 final ExecutableElement m = entry.getValue();
   362                 final boolean isStatic = m.getModifiers().contains(Modifier.STATIC);
   363                 
   364                 source.append("\n  public java.lang.Object ")
   365                     .append(mangled)
   366                     .append("(");
   367                 
   368                 String sep = "";
   369                 if (!isStatic) {
   370                     source.append(((TypeElement)m.getEnclosingElement()).getQualifiedName());
   371                     source.append(" self");
   372                     sep = ", ";
   373                 }
   374                 
   375                 int cnt = 0;
   376                 StringBuilder convert = new StringBuilder();
   377                 for (VariableElement ve : m.getParameters()) {
   378                     source.append(sep);
   379                     ++cnt;
   380                     final TypeMirror t = ve.asType();
   381                     if (!t.getKind().isPrimitive()) {
   382                         source.append("Object");
   383                         convert.append("    if (p instanceof org.apidesign.html.boot.spi.Fn.FromJavaScript) {\n");
   384                         convert.append("      arg").append(cnt).
   385                             append(" = ((org.apidesign.html.boot.spi.Fn.FromJavaScript)p).toJava(arg").append(cnt).
   386                             append(");\n");
   387                         convert.append("    }\n");
   388                     } else {
   389                         source.append(t);
   390                     }
   391                     source.append(" arg").append(cnt);
   392                     sep = ", ";
   393                 }
   394                 source.append(") throws Throwable {\n");
   395                 source.append(convert);
   396                 if (processingEnv.getSourceVersion().compareTo(SourceVersion.RELEASE_7) >= 0) {
   397                     source.append("    try (java.io.Closeable a = org.apidesign.html.boot.spi.Fn.activate(p)) { \n");
   398                 } else {
   399                     source.append("    java.io.Closeable a = org.apidesign.html.boot.spi.Fn.activate(p); try {\n");
   400                 }
   401                 source.append("    ");
   402                 if (m.getReturnType().getKind() != TypeKind.VOID) {
   403                     source.append("Object $ret = ");
   404                 }
   405                 if (isStatic) {
   406                     source.append(((TypeElement)m.getEnclosingElement()).getQualifiedName());
   407                     source.append('.');
   408                 } else {
   409                     source.append("self.");
   410                 }
   411                 source.append(m.getSimpleName());
   412                 source.append("(");
   413                 cnt = 0;
   414                 sep = "";
   415                 for (VariableElement ve : m.getParameters()) {
   416                     source.append(sep);
   417                     source.append("(").append(ve.asType());
   418                     source.append(")arg").append(++cnt);
   419                     sep = ", ";
   420                 }
   421                 source.append(");\n");
   422                 if (m.getReturnType().getKind() == TypeKind.VOID) {
   423                     source.append("    return null;\n");
   424                 } else {
   425                     source.append("    if (p instanceof org.apidesign.html.boot.spi.Fn.ToJavaScript) {\n");
   426                     source.append("      $ret = ((org.apidesign.html.boot.spi.Fn.ToJavaScript)p).toJavaScript($ret);\n");
   427                     source.append("    }\n");
   428                     source.append("    return $ret;\n");
   429                 }
   430                 if (processingEnv.getSourceVersion().compareTo(SourceVersion.RELEASE_7) >= 0) {
   431                     source.append("    }\n");
   432                 } else {
   433                     
   434                     source.append("    } finally {\n");
   435                     source.append("      a.close();\n");
   436                     source.append("    }\n");
   437                 }
   438                 source.append("  }\n");
   439             }
   440             source.append("}\n");
   441             final String srcName = pkgName + ".$JsCallbacks$";
   442             try {
   443                 Writer w = processingEnv.getFiler().createSourceFile(srcName,
   444                     map.values().toArray(new Element[map.size()])
   445                 ).openWriter();
   446                 w.write(source.toString());
   447                 w.close();
   448             } catch (IOException ex) {
   449                 processingEnv.getMessager().printMessage(
   450                     Diagnostic.Kind.ERROR, "Can't write " + srcName + ": " + ex.getMessage()
   451                 );
   452             }
   453         }
   454     }
   455     
   456     private static String findPkg(Element e) {
   457         while (e.getKind() != ElementKind.PACKAGE) {
   458             e = e.getEnclosingElement();
   459         }
   460         return ((PackageElement)e).getQualifiedName().toString();
   461     }
   462     
   463 }