boot/src/main/java/org/apidesign/html/boot/impl/JavaScriptProcesor.java
author Jaroslav Tulach <jaroslav.tulach@apidesign.org>
Tue, 05 Nov 2013 23:06:32 +0100
changeset 322 4a93f2679691
parent 309 7025177bd67e
child 327 2ed628de0f06
permissions -rw-r--r--
Exposing Fn.activate and Fn.activePresenter so they are available from exported packages
     1 /**
     2  * HTML via Java(tm) Language Bindings
     3  * Copyright (C) 2013 Jaroslav Tulach <jaroslav.tulach@apidesign.org>
     4  *
     5  * This program is free software: you can redistribute it and/or modify
     6  * it under the terms of the GNU General Public License as published by
     7  * the Free Software Foundation, version 2 of the License.
     8  *
     9  * This program is distributed in the hope that it will be useful,
    10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  * GNU General Public License for more details. apidesign.org
    13  * designates this particular file as subject to the
    14  * "Classpath" exception as provided by apidesign.org
    15  * in the License file that accompanied this code.
    16  *
    17  * You should have received a copy of the GNU General Public License
    18  * along with this program. Look for COPYING file in the top folder.
    19  * If not, see http://wiki.apidesign.org/wiki/GPLwithClassPathException
    20  */
    21 package org.apidesign.html.boot.impl;
    22 
    23 import java.io.IOException;
    24 import java.io.Writer;
    25 import java.util.Collections;
    26 import java.util.HashMap;
    27 import java.util.HashSet;
    28 import java.util.List;
    29 import java.util.Map;
    30 import java.util.Set;
    31 import java.util.TreeMap;
    32 import javax.annotation.processing.AbstractProcessor;
    33 import javax.annotation.processing.Completion;
    34 import javax.annotation.processing.Completions;
    35 import javax.annotation.processing.Messager;
    36 import javax.annotation.processing.Processor;
    37 import javax.annotation.processing.RoundEnvironment;
    38 import javax.lang.model.element.AnnotationMirror;
    39 import javax.lang.model.element.Element;
    40 import javax.lang.model.element.ElementKind;
    41 import javax.lang.model.element.ExecutableElement;
    42 import javax.lang.model.element.Modifier;
    43 import javax.lang.model.element.PackageElement;
    44 import javax.lang.model.element.TypeElement;
    45 import javax.lang.model.element.VariableElement;
    46 import javax.lang.model.type.ArrayType;
    47 import javax.lang.model.type.ExecutableType;
    48 import javax.lang.model.type.TypeKind;
    49 import javax.lang.model.type.TypeMirror;
    50 import javax.tools.Diagnostic;
    51 import net.java.html.js.JavaScriptBody;
    52 import net.java.html.js.JavaScriptResource;
    53 import org.openide.util.lookup.ServiceProvider;
    54 
    55 /**
    56  *
    57  * @author Jaroslav Tulach <jtulach@netbeans.org>
    58  */
    59 @ServiceProvider(service = Processor.class)
    60 public final class JavaScriptProcesor extends AbstractProcessor {
    61     private final Map<String,Map<String,ExecutableElement>> javacalls = 
    62         new HashMap<String,Map<String,ExecutableElement>>();
    63     
    64     @Override
    65     public Set<String> getSupportedAnnotationTypes() {
    66         Set<String> set = new HashSet<String>();
    67         set.add(JavaScriptBody.class.getName());
    68         set.add(JavaScriptResource.class.getName());
    69         return set;
    70     }
    71     
    72     @Override
    73     public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    74         final Messager msg = processingEnv.getMessager();
    75         for (Element e : roundEnv.getElementsAnnotatedWith(JavaScriptBody.class)) {
    76             if (e.getKind() != ElementKind.METHOD && e.getKind() != ElementKind.CONSTRUCTOR) {
    77                 continue;
    78             }
    79             ExecutableElement ee = (ExecutableElement)e;
    80             List<? extends VariableElement> params = ee.getParameters();
    81             
    82             JavaScriptBody jsb = e.getAnnotation(JavaScriptBody.class);
    83             if (jsb == null) {
    84                 continue;
    85             }
    86             String[] arr = jsb.args();
    87             if (params.size() != arr.length) {
    88                 msg.printMessage(Diagnostic.Kind.ERROR, "Number of args arguments does not match real arguments!", e);
    89             }
    90             if (!jsb.javacall() && jsb.body().contains(".@")) {
    91                 msg.printMessage(Diagnostic.Kind.WARNING, "Usage of .@ usually requires javacall=true", e);
    92             }
    93             if (jsb.javacall()) {
    94                 JsCallback verify = new VerifyCallback(e);
    95                 try {
    96                     verify.parse(jsb.body());
    97                 } catch (IllegalStateException ex) {
    98                     msg.printMessage(Diagnostic.Kind.ERROR, ex.getLocalizedMessage(), e);
    99                 }
   100             }
   101         }
   102         if (roundEnv.processingOver()) {
   103             generateCallbackClass(javacalls);
   104             javacalls.clear();
   105         }
   106         return true;
   107     }
   108 
   109     @Override
   110     public Iterable<? extends Completion> getCompletions(Element e, 
   111         AnnotationMirror annotation, ExecutableElement member, String userText
   112     ) {
   113         StringBuilder sb = new StringBuilder();
   114         if (e.getKind() == ElementKind.METHOD && member.getSimpleName().contentEquals("args")) {
   115             ExecutableElement ee = (ExecutableElement) e;
   116             String sep = "";
   117             sb.append("{ ");
   118             for (VariableElement ve : ee.getParameters()) {
   119                 sb.append(sep).append('"').append(ve.getSimpleName())
   120                     .append('"');
   121                 sep = ", ";
   122             }
   123             sb.append(" }");
   124             return Collections.nCopies(1, Completions.of(sb.toString()));
   125         }
   126         return null;
   127     }
   128 
   129     private class VerifyCallback extends JsCallback {
   130         private final Element e;
   131         public VerifyCallback(Element e) {
   132             this.e = e;
   133         }
   134 
   135         @Override
   136         protected CharSequence callMethod(String ident, String fqn, String method, String params) {
   137             final TypeElement type = processingEnv.getElementUtils().getTypeElement(fqn);
   138             if (type == null) {
   139                 processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
   140                     "Callback to non-existing class " + fqn, e
   141                 );
   142                 return "";
   143             }
   144             ExecutableElement found = null;
   145             StringBuilder foundParams = new StringBuilder();
   146             for (Element m : type.getEnclosedElements()) {
   147                 if (m.getKind() != ElementKind.METHOD) {
   148                     continue;
   149                 }
   150                 if (m.getSimpleName().contentEquals(method)) {
   151                     String paramTypes = findParamTypes((ExecutableElement)m);
   152                     if (paramTypes.equals(params)) {
   153                         found = (ExecutableElement) m;
   154                         break;
   155                     }
   156                     foundParams.append(paramTypes).append("\n");
   157                 }
   158             }
   159             if (found == null) {
   160                 if (foundParams.length() == 0) {
   161                     processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
   162                         "Callback to class " + fqn + " with unknown method " + method, e
   163                     );
   164                 } else {
   165                     processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, 
   166                         "Callback to " + fqn + "." + method + " with wrong parameters: " + 
   167                         params + ". Only known parameters are " + foundParams, e
   168                     );
   169                 }
   170             } else {
   171                 Map<String,ExecutableElement> mangledOnes = javacalls.get(findPkg(e));
   172                 if (mangledOnes == null) {
   173                     mangledOnes = new TreeMap<String, ExecutableElement>();
   174                     javacalls.put(findPkg(e), mangledOnes);
   175                 }
   176                 String mangled = JsCallback.mangle(fqn, method, findParamTypes(found));
   177                 mangledOnes.put(mangled, found);
   178             }
   179             return "";
   180         }
   181 
   182         private String findParamTypes(ExecutableElement method) {
   183             ExecutableType t = (ExecutableType) method.asType();
   184             StringBuilder sb = new StringBuilder();
   185             sb.append('(');
   186             for (TypeMirror tm : t.getParameterTypes()) {
   187                 if (tm.getKind().isPrimitive()) {
   188                     switch (tm.getKind()) {
   189                         case INT: sb.append('I'); break;
   190                         case BOOLEAN: sb.append('Z'); break;
   191                         case BYTE: sb.append('B'); break;
   192                         case CHAR: sb.append('C'); break;
   193                         case SHORT: sb.append('S'); break;
   194                         case DOUBLE: sb.append('D'); break;
   195                         case FLOAT: sb.append('F'); break;
   196                         case LONG: sb.append('J'); break;
   197                         default:
   198                             throw new IllegalStateException("Uknown " + tm.getKind());
   199                     }
   200                 } else {
   201                     while (tm.getKind() == TypeKind.ARRAY) {
   202                         sb.append('[');
   203                         tm = ((ArrayType)tm).getComponentType();
   204                     }
   205                     sb.append('L');
   206                     sb.append(tm.toString().replace('.', '/'));
   207                     sb.append(';');
   208                 }
   209             }
   210             sb.append(')');
   211             return sb.toString();
   212         }
   213     }
   214     
   215     private void generateCallbackClass(Map<String,Map<String, ExecutableElement>> process) {
   216         for (Map.Entry<String, Map<String, ExecutableElement>> pkgEn : process.entrySet()) {
   217             String pkgName = pkgEn.getKey();
   218             Map<String, ExecutableElement> map = pkgEn.getValue();
   219             StringBuilder source = new StringBuilder();
   220             source.append("package ").append(pkgName).append(";\n");
   221             source.append("public final class $JsCallbacks$ {\n");
   222             source.append("  static final $JsCallbacks$ VM = new $JsCallbacks$(null);\n");
   223             source.append("  private final org.apidesign.html.boot.spi.Fn.Presenter p;\n");
   224             source.append("  private $JsCallbacks$ last;\n");
   225             source.append("  private $JsCallbacks$(org.apidesign.html.boot.spi.Fn.Presenter p) {\n");
   226             source.append("    this.p = p;\n");
   227             source.append("  }\n");
   228             source.append("  final $JsCallbacks$ current() {\n");
   229             source.append("    org.apidesign.html.boot.spi.Fn.Presenter now = org.apidesign.html.boot.spi.Fn.activePresenter();\n");
   230             source.append("    if (now == p) return this;\n");
   231             source.append("    if (last != null && now == last.p) return last;\n");
   232             source.append("    return last = new $JsCallbacks$(now);\n");
   233             source.append("  }\n");
   234             for (Map.Entry<String, ExecutableElement> entry : map.entrySet()) {
   235                 final String mangled = entry.getKey();
   236                 final ExecutableElement m = entry.getValue();
   237                 final boolean isStatic = m.getModifiers().contains(Modifier.STATIC);
   238                 
   239                 source.append("\n  public java.lang.Object ")
   240                     .append(mangled)
   241                     .append("(");
   242                 
   243                 String sep = "";
   244                 if (!isStatic) {
   245                     source.append(((TypeElement)m.getEnclosingElement()).getQualifiedName());
   246                     source.append(" self");
   247                     sep = ", ";
   248                 }
   249                 
   250                 int cnt = 0;
   251                 for (VariableElement ve : m.getParameters()) {
   252                     source.append(sep);
   253                     source.append(ve.asType());
   254                     source.append(" arg").append(++cnt);
   255                     sep = ", ";
   256                 }
   257                 source.append(") throws Throwable {\n");
   258                 source.append("    try (java.io.Closeable a = org.apidesign.html.boot.spi.Fn.activate(p)) { \n");
   259                 source.append("    ");
   260                 if (m.getReturnType().getKind() != TypeKind.VOID) {
   261                     source.append("return ");
   262                 }
   263                 if (isStatic) {
   264                     source.append(((TypeElement)m.getEnclosingElement()).getQualifiedName());
   265                     source.append('.');
   266                 } else {
   267                     source.append("self.");
   268                 }
   269                 source.append(m.getSimpleName());
   270                 source.append("(");
   271                 cnt = 0;
   272                 sep = "";
   273                 for (VariableElement ve : m.getParameters()) {
   274                     source.append(sep);
   275                     source.append("arg").append(++cnt);
   276                     sep = ", ";
   277                 }
   278                 source.append(");\n");
   279                 if (m.getReturnType().getKind() == TypeKind.VOID) {
   280                     source.append("    return null;\n");
   281                 }
   282                 source.append("    }\n");
   283                 source.append("  }\n");
   284             }
   285             source.append("}\n");
   286             final String srcName = pkgName + ".$JsCallbacks$";
   287             try {
   288                 Writer w = processingEnv.getFiler().createSourceFile(srcName,
   289                     map.values().toArray(new Element[map.size()])
   290                 ).openWriter();
   291                 w.write(source.toString());
   292                 w.close();
   293             } catch (IOException ex) {
   294                 processingEnv.getMessager().printMessage(
   295                     Diagnostic.Kind.ERROR, "Can't write " + srcName + ": " + ex.getMessage()
   296                 );
   297             }
   298         }
   299     }
   300     
   301     private static String findPkg(Element e) {
   302         while (e.getKind() != ElementKind.PACKAGE) {
   303             e = e.getEnclosingElement();
   304         }
   305         return ((PackageElement)e).getQualifiedName().toString();
   306     }
   307     
   308 }