javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java
author Jaroslav Tulach <jaroslav.tulach@apidesign.org>
Mon, 08 Apr 2013 12:12:42 +0200
branchmodel
changeset 949 3bd43aa6f08d
parent 947 26f7eeb81aec
child 950 445d5f1d4177
permissions -rw-r--r--
OnPropertyChange allows us to update the tweets as soon as active tweets are changed
     1 /**
     2  * Back 2 Browser Bytecode Translator
     3  * Copyright (C) 2012 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.
    13  *
    14  * You should have received a copy of the GNU General Public License
    15  * along with this program. Look for COPYING file in the top folder.
    16  * If not, see http://opensource.org/licenses/GPL-2.0.
    17  */
    18 package org.apidesign.bck2brwsr.htmlpage;
    19 
    20 import java.io.IOException;
    21 import java.io.InputStream;
    22 import java.io.OutputStreamWriter;
    23 import java.io.StringWriter;
    24 import java.io.Writer;
    25 import java.util.ArrayList;
    26 import java.util.Collection;
    27 import java.util.Collections;
    28 import java.util.HashMap;
    29 import java.util.HashSet;
    30 import java.util.LinkedHashSet;
    31 import java.util.List;
    32 import java.util.Map;
    33 import java.util.Set;
    34 import java.util.WeakHashMap;
    35 import javax.annotation.processing.AbstractProcessor;
    36 import javax.annotation.processing.Completion;
    37 import javax.annotation.processing.Completions;
    38 import javax.annotation.processing.Messager;
    39 import javax.annotation.processing.Processor;
    40 import javax.annotation.processing.RoundEnvironment;
    41 import javax.annotation.processing.SupportedAnnotationTypes;
    42 import javax.lang.model.element.AnnotationMirror;
    43 import javax.lang.model.element.Element;
    44 import javax.lang.model.element.ElementKind;
    45 import javax.lang.model.element.ExecutableElement;
    46 import javax.lang.model.element.Modifier;
    47 import javax.lang.model.element.PackageElement;
    48 import javax.lang.model.element.TypeElement;
    49 import javax.lang.model.element.VariableElement;
    50 import javax.lang.model.type.ArrayType;
    51 import javax.lang.model.type.DeclaredType;
    52 import javax.lang.model.type.MirroredTypeException;
    53 import javax.lang.model.type.TypeKind;
    54 import javax.lang.model.type.TypeMirror;
    55 import javax.lang.model.util.Elements;
    56 import javax.lang.model.util.Types;
    57 import javax.tools.Diagnostic;
    58 import javax.tools.FileObject;
    59 import javax.tools.StandardLocation;
    60 import org.apidesign.bck2brwsr.htmlpage.api.ComputedProperty;
    61 import org.apidesign.bck2brwsr.htmlpage.api.Model;
    62 import org.apidesign.bck2brwsr.htmlpage.api.On;
    63 import org.apidesign.bck2brwsr.htmlpage.api.OnFunction;
    64 import org.apidesign.bck2brwsr.htmlpage.api.OnPropertyChange;
    65 import org.apidesign.bck2brwsr.htmlpage.api.OnReceive;
    66 import org.apidesign.bck2brwsr.htmlpage.api.Page;
    67 import org.apidesign.bck2brwsr.htmlpage.api.Property;
    68 import org.openide.util.lookup.ServiceProvider;
    69 
    70 /** Annotation processor to process an XHTML page and generate appropriate 
    71  * "id" file.
    72  *
    73  * @author Jaroslav Tulach <jtulach@netbeans.org>
    74  */
    75 @ServiceProvider(service=Processor.class)
    76 @SupportedAnnotationTypes({
    77     "org.apidesign.bck2brwsr.htmlpage.api.Model",
    78     "org.apidesign.bck2brwsr.htmlpage.api.Page",
    79     "org.apidesign.bck2brwsr.htmlpage.api.OnFunction",
    80     "org.apidesign.bck2brwsr.htmlpage.api.OnReceive",
    81     "org.apidesign.bck2brwsr.htmlpage.api.OnPropertyChange",
    82     "org.apidesign.bck2brwsr.htmlpage.api.On"
    83 })
    84 public final class PageProcessor extends AbstractProcessor {
    85     private final Map<Element,String> models = new WeakHashMap<>();
    86     @Override
    87     public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    88         boolean ok = true;
    89         for (Element e : roundEnv.getElementsAnnotatedWith(Model.class)) {
    90             if (!processModel(e)) {
    91                 ok = false;
    92             }
    93         }
    94         for (Element e : roundEnv.getElementsAnnotatedWith(Page.class)) {
    95             if (!processPage(e)) {
    96                 ok = false;
    97             }
    98         }
    99         if (roundEnv.processingOver()) {
   100             models.clear();
   101         }
   102         return ok;
   103     }
   104 
   105     private InputStream openStream(String pkg, String name) throws IOException {
   106         try {
   107             FileObject fo = processingEnv.getFiler().getResource(
   108                 StandardLocation.SOURCE_PATH, pkg, name);
   109             return fo.openInputStream();
   110         } catch (IOException ex) {
   111             return processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, pkg, name).openInputStream();
   112         }
   113     }
   114 
   115     private  Messager err() {
   116         return processingEnv.getMessager();
   117     }
   118     
   119     private boolean processModel(Element e) {
   120         boolean ok = true;
   121         Model m = e.getAnnotation(Model.class);
   122         if (m == null) {
   123             return true;
   124         }
   125         String pkg = findPkgName(e);
   126         Writer w;
   127         String className = m.className();
   128         models.put(e, className);
   129         try {
   130             StringWriter body = new StringWriter();
   131             List<String> propsGetSet = new ArrayList<>();
   132             List<String> functions = new ArrayList<>();
   133             Map<String, Collection<String>> propsDeps = new HashMap<>();
   134             Map<String, Collection<String>> functionDeps = new HashMap<>();
   135             if (!generateComputedProperties(body, m.properties(), e.getEnclosedElements(), propsGetSet, propsDeps)) {
   136                 ok = false;
   137             }
   138             if (!generateOnChange(e, propsDeps, m.properties(), className, functionDeps)) {
   139                 ok = false;
   140             }
   141             if (!generateProperties(e, body, m.properties(), propsGetSet, propsDeps, functionDeps)) {
   142                 ok = false;
   143             }
   144             if (!generateFunctions(e, body, className, e.getEnclosedElements(), functions)) {
   145                 ok = false;
   146             }
   147             FileObject java = processingEnv.getFiler().createSourceFile(pkg + '.' + className, e);
   148             w = new OutputStreamWriter(java.openOutputStream());
   149             try {
   150                 w.append("package " + pkg + ";\n");
   151                 w.append("import org.apidesign.bck2brwsr.htmlpage.api.*;\n");
   152                 w.append("import org.apidesign.bck2brwsr.htmlpage.KOList;\n");
   153                 w.append("import org.apidesign.bck2brwsr.core.JavaScriptOnly;\n");
   154                 w.append("final class ").append(className).append(" implements Cloneable {\n");
   155                 w.append("  private boolean locked;\n");
   156                 w.append("  private org.apidesign.bck2brwsr.htmlpage.Knockout ko;\n");
   157                 w.append(body.toString());
   158                 w.append("  private static Class<" + inPckName(e) + "> modelFor() { return null; }\n");
   159                 w.append("  public ").append(className).append("() {\n");
   160                 w.append("    intKnckt();\n");
   161                 w.append("  };\n");
   162                 w.append("  private void intKnckt() {\n");
   163                 w.append("    ko = org.apidesign.bck2brwsr.htmlpage.Knockout.applyBindings(this, ");
   164                 writeStringArray(propsGetSet, w);
   165                 w.append(", ");
   166                 writeStringArray(functions, w);
   167                 w.append("    );\n");
   168                 w.append("  };\n");
   169                 w.append("  ").append(className).append("(Object json) {\n");
   170                 int values = 0;
   171                 for (int i = 0; i < propsGetSet.size(); i += 4) {
   172                     Property p = findProperty(m.properties(), propsGetSet.get(i));
   173                     if (p == null) {
   174                         continue;
   175                     }
   176                     values++;
   177                 }
   178                 w.append("    Object[] ret = new Object[" + values + "];\n");
   179                 w.append("    org.apidesign.bck2brwsr.htmlpage.ConvertTypes.extractJSON(json, new String[] {\n");
   180                 for (int i = 0; i < propsGetSet.size(); i += 4) {
   181                     Property p = findProperty(m.properties(), propsGetSet.get(i));
   182                     if (p == null) {
   183                         continue;
   184                     }
   185                     w.append("      \"").append(propsGetSet.get(i)).append("\",\n");
   186                 }
   187                 w.append("    }, ret);\n");
   188                 for (int i = 0, cnt = 0, prop = 0; i < propsGetSet.size(); i += 4) {
   189                     final String pn = propsGetSet.get(i);
   190                     Property p = findProperty(m.properties(), pn);
   191                     if (p == null) {
   192                         continue;
   193                     }
   194                     boolean[] isModel = { false };
   195                     boolean[] isEnum = { false };
   196                     String type = checkType(m.properties()[prop++], isModel, isEnum);
   197                     if (isEnum[0]) {
   198 //                        w.append(type).append(".valueOf((String)");
   199 //                        close = true;
   200                         w.append("    this.prop_").append(pn);
   201                         w.append(" = null;\n");
   202                     } else if (p.array()) {
   203                         w.append("if (ret[" + cnt + "] instanceof Object[]) {\n");
   204                         w.append("  for (Object e : ((Object[])ret[" + cnt + "])) {\n");
   205                         if (isModel[0]) {
   206                             w.append("    this.prop_").append(pn).append(".add(new ");
   207                             w.append(type).append("(e));\n");
   208                         } else {
   209                             if (isPrimitive(type)) {
   210                                 w.append("    this.prop_").append(pn).append(".add(((Number)e).");
   211                                 w.append(type).append("Value());\n");
   212                             } else {
   213                                 w.append("    this.prop_").append(pn).append(".add((");
   214                                 w.append(type).append(")e);\n");
   215                             }
   216                         }
   217                         w.append("  }\n");
   218                         w.append("}\n");
   219                     } else {
   220                         if (isPrimitive(type)) {
   221                             w.append("    this.prop_").append(pn);
   222                             w.append(" = ((Number)").append("ret[" + cnt + "]).");
   223                             w.append(type).append("Value();\n");
   224                         } else {
   225                             w.append("    this.prop_").append(pn);
   226                             w.append(" = (").append(type).append(')');
   227                             w.append("ret[" + cnt + "];\n");
   228                         }
   229                     }
   230                     cnt++;
   231                 }
   232                 w.append("    intKnckt();\n");
   233                 w.append("  };\n");
   234                 writeToString(m.properties(), w);
   235                 writeClone(className, m.properties(), w);
   236                 w.append("}\n");
   237             } finally {
   238                 w.close();
   239             }
   240         } catch (IOException ex) {
   241             err().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e);
   242             return false;
   243         }
   244         return ok;
   245     }
   246     
   247     private boolean processPage(Element e) {
   248         boolean ok = true;
   249         Page p = e.getAnnotation(Page.class);
   250         if (p == null) {
   251             return true;
   252         }
   253         String pkg = findPkgName(e);
   254 
   255         ProcessPage pp;
   256         try (InputStream is = openStream(pkg, p.xhtml())) {
   257             pp = ProcessPage.readPage(is);
   258             is.close();
   259         } catch (IOException iOException) {
   260             err().printMessage(Diagnostic.Kind.ERROR, "Can't read " + p.xhtml() + " as " + iOException.getMessage(), e);
   261             ok = false;
   262             pp = null;
   263         }
   264         Writer w;
   265         String className = p.className();
   266         if (className.isEmpty()) {
   267             int indx = p.xhtml().indexOf('.');
   268             className = p.xhtml().substring(0, indx);
   269         }
   270         try {
   271             StringWriter body = new StringWriter();
   272             List<String> propsGetSet = new ArrayList<>();
   273             List<String> functions = new ArrayList<>();
   274             Map<String, Collection<String>> propsDeps = new HashMap<>();
   275             Map<String, Collection<String>> functionDeps = new HashMap<>();
   276             if (!generateComputedProperties(body, p.properties(), e.getEnclosedElements(), propsGetSet, propsDeps)) {
   277                 ok = false;
   278             }
   279             if (!generateOnChange(e, propsDeps, p.properties(), className, functionDeps)) {
   280                 ok = false;
   281             }
   282             if (!generateProperties(e, body, p.properties(), propsGetSet, propsDeps, functionDeps)) {
   283                 ok = false;
   284             }
   285             if (!generateFunctions(e, body, className, e.getEnclosedElements(), functions)) {
   286                 ok = false;
   287             }
   288             if (!generateReceive(e, body, className, e.getEnclosedElements(), functions)) {
   289                 ok = false;
   290             }
   291             
   292             FileObject java = processingEnv.getFiler().createSourceFile(pkg + '.' + className, e);
   293             w = new OutputStreamWriter(java.openOutputStream());
   294             try {
   295                 w.append("package " + pkg + ";\n");
   296                 w.append("import org.apidesign.bck2brwsr.htmlpage.api.*;\n");
   297                 w.append("import org.apidesign.bck2brwsr.htmlpage.KOList;\n");
   298                 w.append("final class ").append(className).append(" {\n");
   299                 w.append("  private boolean locked;\n");
   300                 if (!initializeOnClick(className, (TypeElement) e, w, pp)) {
   301                     ok = false;
   302                 } else {
   303                     if (pp != null) for (String id : pp.ids()) {
   304                         String tag = pp.tagNameForId(id);
   305                         String type = type(tag);
   306                         w.append("  ").append("public final ").
   307                             append(type).append(' ').append(cnstnt(id)).append(" = new ").
   308                             append(type).append("(\"").append(id).append("\");\n");
   309                     }
   310                 }
   311                 w.append("  private org.apidesign.bck2brwsr.htmlpage.Knockout ko;\n");
   312                 w.append(body.toString());
   313                 if (!propsGetSet.isEmpty()) {
   314                     w.write("public " + className + " applyBindings() {\n");
   315                     w.write("  ko = org.apidesign.bck2brwsr.htmlpage.Knockout.applyBindings(");
   316                     w.write(className + ".class, this, ");
   317                     writeStringArray(propsGetSet, w);
   318                     w.append(", ");
   319                     writeStringArray(functions, w);
   320                     w.write(");\n  return this;\n}\n");
   321 
   322                     w.write("public void triggerEvent(Element e, OnEvent ev) {\n");
   323                     w.write("  org.apidesign.bck2brwsr.htmlpage.Knockout.triggerEvent(e.getId(), ev.getElementPropertyName());\n");
   324                     w.write("}\n");
   325                 }
   326                 w.append("}\n");
   327             } finally {
   328                 w.close();
   329             }
   330         } catch (IOException ex) {
   331             err().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e);
   332             return false;
   333         }
   334         return ok;
   335     }
   336 
   337     private static String type(String tag) {
   338         if (tag.equals("title")) {
   339             return "Title";
   340         }
   341         if (tag.equals("button")) {
   342             return "Button";
   343         }
   344         if (tag.equals("input")) {
   345             return "Input";
   346         }
   347         if (tag.equals("canvas")) {
   348             return "Canvas";
   349         }
   350         if (tag.equals("img")) {
   351             return "Image";
   352         }
   353         return "Element";
   354     }
   355 
   356     private static String cnstnt(String id) {
   357         return id.replace('.', '_').replace('-', '_');
   358     }
   359 
   360     private boolean initializeOnClick(
   361         String className, TypeElement type, Writer w, ProcessPage pp
   362     ) throws IOException {
   363         boolean ok = true;
   364         TypeMirror stringType = processingEnv.getElementUtils().getTypeElement("java.lang.String").asType();
   365         { //for (Element clazz : pe.getEnclosedElements()) {
   366           //  if (clazz.getKind() != ElementKind.CLASS) {
   367             //    continue;
   368            // }
   369             w.append("  public ").append(className).append("() {\n");
   370             StringBuilder dispatch = new StringBuilder();
   371             int dispatchCnt = 0;
   372             for (Element method : type.getEnclosedElements()) {
   373                 On oc = method.getAnnotation(On.class);
   374                 if (oc != null) {
   375                     for (String id : oc.id()) {
   376                         if (pp == null) {
   377                             err().printMessage(Diagnostic.Kind.ERROR, "id = " + id + " not found in HTML page.");
   378                             ok = false;
   379                             continue;
   380                         }
   381                         if (pp.tagNameForId(id) == null) {
   382                             err().printMessage(Diagnostic.Kind.ERROR, "id = " + id + " does not exist in the HTML page. Found only " + pp.ids(), method);
   383                             ok = false;
   384                             continue;
   385                         }
   386                         ExecutableElement ee = (ExecutableElement)method;
   387                         CharSequence params = wrapParams(ee, id, className, "ev", null);
   388                         if (!ee.getModifiers().contains(Modifier.STATIC)) {
   389                             err().printMessage(Diagnostic.Kind.ERROR, "@On method has to be static", ee);
   390                             ok = false;
   391                             continue;
   392                         }
   393                         if (ee.getModifiers().contains(Modifier.PRIVATE)) {
   394                             err().printMessage(Diagnostic.Kind.ERROR, "@On method can't be private", ee);
   395                             ok = false;
   396                             continue;
   397                         }
   398                         w.append("  OnEvent." + oc.event()).append(".of(").append(cnstnt(id)).
   399                             append(").perform(new OnDispatch(" + dispatchCnt + "));\n");
   400 
   401                         dispatch.
   402                             append("      case ").append(dispatchCnt).append(": ").
   403                             append(type.getSimpleName().toString()).
   404                             append('.').append(ee.getSimpleName()).append("(").
   405                             append(params).
   406                             append("); break;\n");
   407                         
   408                         dispatchCnt++;
   409                     }
   410                 }
   411             }
   412             w.append("  }\n");
   413             if (dispatchCnt > 0) {
   414                 w.append("class OnDispatch implements OnHandler {\n");
   415                 w.append("  private final int dispatch;\n");
   416                 w.append("  OnDispatch(int d) { dispatch = d; }\n");
   417                 w.append("  public void onEvent(Object ev) {\n");
   418                 w.append("    switch (dispatch) {\n");
   419                 w.append(dispatch);
   420                 w.append("    }\n");
   421                 w.append("  }\n");
   422                 w.append("}\n");
   423             }
   424             
   425 
   426         }
   427         return ok;
   428     }
   429 
   430     @Override
   431     public Iterable<? extends Completion> getCompletions(
   432         Element element, AnnotationMirror annotation, 
   433         ExecutableElement member, String userText
   434     ) {
   435         if (!userText.startsWith("\"")) {
   436             return Collections.emptyList();
   437         }
   438         
   439         Element cls = findClass(element);
   440         Page p = cls.getAnnotation(Page.class);
   441         String pkg = findPkgName(cls);
   442         ProcessPage pp;
   443         try {
   444             InputStream is = openStream(pkg, p.xhtml());
   445             pp = ProcessPage.readPage(is);
   446             is.close();
   447         } catch (IOException iOException) {
   448             return Collections.emptyList();
   449         }
   450         
   451         List<Completion> cc = new ArrayList<>();
   452         userText = userText.substring(1);
   453         for (String id : pp.ids()) {
   454             if (id.startsWith(userText)) {
   455                 cc.add(Completions.of("\"" + id + "\"", id));
   456             }
   457         }
   458         return cc;
   459     }
   460     
   461     private static Element findClass(Element e) {
   462         if (e == null) {
   463             return null;
   464         }
   465         Page p = e.getAnnotation(Page.class);
   466         if (p != null) {
   467             return e;
   468         }
   469         return e.getEnclosingElement();
   470     }
   471 
   472     private boolean generateProperties(
   473         Element where,
   474         Writer w, Property[] properties,
   475         Collection<String> props, 
   476         Map<String,Collection<String>> deps,
   477         Map<String,Collection<String>> functionDeps
   478     ) throws IOException {
   479         boolean ok = true;
   480         for (Property p : properties) {
   481             final String tn;
   482             tn = typeName(where, p);
   483             String[] gs = toGetSet(p.name(), tn, p.array());
   484 
   485             if (p.array()) {
   486                 w.write("private KOList<" + tn + "> prop_" + p.name() + " = new KOList<" + tn + ">(\""
   487                     + p.name() + "\"");
   488                 final Collection<String> dependants = deps.get(p.name());
   489                 if (dependants != null) {
   490                     for (String depProp : dependants) {
   491                         w.write(", ");
   492                         w.write('\"');
   493                         w.write(depProp);
   494                         w.write('\"');
   495                     }
   496                 }
   497                 w.write(");\n");
   498                 w.write("public java.util.List<" + tn + "> " + gs[0] + "() {\n");
   499                 w.write("  if (locked) throw new IllegalStateException();\n");
   500                 w.write("  prop_" + p.name() + ".assign(ko);\n");
   501                 w.write("  return prop_" + p.name() + ";\n");
   502                 w.write("}\n");
   503             } else {
   504                 w.write("private " + tn + " prop_" + p.name() + ";\n");
   505                 w.write("public " + tn + " " + gs[0] + "() {\n");
   506                 w.write("  if (locked) throw new IllegalStateException();\n");
   507                 w.write("  return prop_" + p.name() + ";\n");
   508                 w.write("}\n");
   509                 w.write("public void " + gs[1] + "(" + tn + " v) {\n");
   510                 w.write("  if (locked) throw new IllegalStateException();\n");
   511                 w.write("  prop_" + p.name() + " = v;\n");
   512                 w.write("  if (ko != null) {\n");
   513                 w.write("    ko.valueHasMutated(\"" + p.name() + "\");\n");
   514                 Collection<String> dependants = deps.get(p.name());
   515                 if (dependants != null) {
   516                     for (String depProp : dependants) {
   517                         w.write("    ko.valueHasMutated(\"" + depProp + "\");\n");
   518                     }
   519                 }
   520                 w.write("  }\n");
   521                 dependants = functionDeps.get(p.name());
   522                 if (dependants != null) {
   523                     for (String call : dependants) {
   524                         w.append(call);
   525                     }
   526                 }
   527                 w.write("}\n");
   528             }
   529             
   530             props.add(p.name());
   531             props.add(gs[2]);
   532             props.add(gs[3]);
   533             props.add(gs[0]);
   534         }
   535         return ok;
   536     }
   537 
   538     private boolean generateComputedProperties(
   539         Writer w, Property[] fixedProps,
   540         Collection<? extends Element> arr, Collection<String> props,
   541         Map<String,Collection<String>> deps
   542     ) throws IOException {
   543         boolean ok = true;
   544         for (Element e : arr) {
   545             if (e.getKind() != ElementKind.METHOD) {
   546                 continue;
   547             }
   548             if (e.getAnnotation(ComputedProperty.class) == null) {
   549                 continue;
   550             }
   551             ExecutableElement ee = (ExecutableElement)e;
   552             final TypeMirror rt = ee.getReturnType();
   553             final Types tu = processingEnv.getTypeUtils();
   554             TypeMirror ert = tu.erasure(rt);
   555             String tn = fqn(ert, ee);
   556             boolean array = false;
   557             if (tn.equals("java.util.List")) {
   558                 array = true;
   559             }
   560             
   561             final String sn = ee.getSimpleName().toString();
   562             String[] gs = toGetSet(sn, tn, array);
   563             
   564             w.write("public " + tn + " " + gs[0] + "() {\n");
   565             w.write("  if (locked) throw new IllegalStateException();\n");
   566             int arg = 0;
   567             for (VariableElement pe : ee.getParameters()) {
   568                 final String dn = pe.getSimpleName().toString();
   569                 
   570                 if (!verifyPropName(pe, dn, fixedProps)) {
   571                     ok = false;
   572                 }
   573                 
   574                 final String dt = fqn(pe.asType(), ee);
   575                 String[] call = toGetSet(dn, dt, false);
   576                 w.write("  " + dt + " arg" + (++arg) + " = ");
   577                 w.write(call[0] + "();\n");
   578                 
   579                 Collection<String> depends = deps.get(dn);
   580                 if (depends == null) {
   581                     depends = new LinkedHashSet<>();
   582                     deps.put(dn, depends);
   583                 }
   584                 depends.add(sn);
   585             }
   586             w.write("  try {\n");
   587             w.write("    locked = true;\n");
   588             w.write("    return " + e.getEnclosingElement().getSimpleName() + '.' + e.getSimpleName() + "(");
   589             String sep = "";
   590             for (int i = 1; i <= arg; i++) {
   591                 w.write(sep);
   592                 w.write("arg" + i);
   593                 sep = ", ";
   594             }
   595             w.write(");\n");
   596             w.write("  } finally {\n");
   597             w.write("    locked = false;\n");
   598             w.write("  }\n");
   599             w.write("}\n");
   600 
   601             props.add(e.getSimpleName().toString());
   602             props.add(gs[2]);
   603             props.add(null);
   604             props.add(gs[0]);
   605         }
   606         
   607         return ok;
   608     }
   609 
   610     private static String[] toGetSet(String name, String type, boolean array) {
   611         String n = Character.toUpperCase(name.charAt(0)) + name.substring(1);
   612         String bck2brwsrType = "L" + type.replace('.', '_') + "_2";
   613         if ("int".equals(type)) {
   614             bck2brwsrType = "I";
   615         }
   616         if ("double".equals(type)) {
   617             bck2brwsrType = "D";
   618         }
   619         String pref = "get";
   620         if ("boolean".equals(type)) {
   621             pref = "is";
   622             bck2brwsrType = "Z";
   623         }
   624         final String nu = n.replace('.', '_');
   625         if (array) {
   626             return new String[] { 
   627                 "get" + n,
   628                 null,
   629                 "get" + nu + "__Ljava_util_List_2",
   630                 null
   631             };
   632         }
   633         return new String[]{
   634             pref + n, 
   635             "set" + n, 
   636             pref + nu + "__" + bck2brwsrType,
   637             "set" + nu + "__V" + bck2brwsrType
   638         };
   639     }
   640 
   641     private String typeName(Element where, Property p) {
   642         String ret;
   643         boolean[] isModel = { false };
   644         boolean[] isEnum = { false };
   645         ret = checkType(p, isModel, isEnum);
   646         if (p.array()) {
   647             String bt = findBoxedType(ret);
   648             if (bt != null) {
   649                 return bt;
   650             }
   651         }
   652         if (!isModel[0] && !"java.lang.String".equals(ret) && !isEnum[0]) {
   653             String bt = findBoxedType(ret);
   654             if (bt == null) {
   655                 err().printMessage(
   656                     Diagnostic.Kind.ERROR, 
   657                     "Only primitive types supported in the mapping. Not " + ret,
   658                     where
   659                 );
   660             }
   661         }
   662         return ret;
   663     }
   664     
   665     private static String findBoxedType(String ret) {
   666         if (ret.equals("boolean")) {
   667             return Boolean.class.getName();
   668         }
   669         if (ret.equals("byte")) {
   670             return Byte.class.getName();
   671         }
   672         if (ret.equals("short")) {
   673             return Short.class.getName();
   674         }
   675         if (ret.equals("char")) {
   676             return Character.class.getName();
   677         }
   678         if (ret.equals("int")) {
   679             return Integer.class.getName();
   680         }
   681         if (ret.equals("long")) {
   682             return Long.class.getName();
   683         }
   684         if (ret.equals("float")) {
   685             return Float.class.getName();
   686         }
   687         if (ret.equals("double")) {
   688             return Double.class.getName();
   689         }
   690         return null;
   691     }
   692 
   693     private boolean verifyPropName(Element e, String propName, Property[] existingProps) {
   694         StringBuilder sb = new StringBuilder();
   695         String sep = "";
   696         for (Property property : existingProps) {
   697             if (property.name().equals(propName)) {
   698                 return true;
   699             }
   700             sb.append(sep);
   701             sb.append('"');
   702             sb.append(property.name());
   703             sb.append('"');
   704             sep = ", ";
   705         }
   706         err().printMessage(Diagnostic.Kind.ERROR,
   707             propName + " is not one of known properties: " + sb
   708             , e
   709         );
   710         return false;
   711     }
   712 
   713     private static String findPkgName(Element e) {
   714         for (;;) {
   715             if (e.getKind() == ElementKind.PACKAGE) {
   716                 return ((PackageElement)e).getQualifiedName().toString();
   717             }
   718             e = e.getEnclosingElement();
   719         }
   720     }
   721 
   722     private boolean generateFunctions(
   723         Element clazz, StringWriter body, String className, 
   724         List<? extends Element> enclosedElements, List<String> functions
   725     ) {
   726         for (Element m : enclosedElements) {
   727             if (m.getKind() != ElementKind.METHOD) {
   728                 continue;
   729             }
   730             ExecutableElement e = (ExecutableElement)m;
   731             OnFunction onF = e.getAnnotation(OnFunction.class);
   732             if (onF == null) {
   733                 continue;
   734             }
   735             if (!e.getModifiers().contains(Modifier.STATIC)) {
   736                 err().printMessage(
   737                     Diagnostic.Kind.ERROR, "@OnFunction method needs to be static", e
   738                 );
   739                 return false;
   740             }
   741             if (e.getModifiers().contains(Modifier.PRIVATE)) {
   742                 err().printMessage(
   743                     Diagnostic.Kind.ERROR, "@OnFunction method cannot be private", e
   744                 );
   745                 return false;
   746             }
   747             if (e.getReturnType().getKind() != TypeKind.VOID) {
   748                 err().printMessage(
   749                     Diagnostic.Kind.ERROR, "@OnFunction method should return void", e
   750                 );
   751                 return false;
   752             }
   753             String n = e.getSimpleName().toString();
   754             body.append("private void ").append(n).append("(Object data, Object ev) {\n");
   755             body.append("  ").append(clazz.getSimpleName()).append(".").append(n).append("(");
   756             body.append(wrapParams(e, null, className, "ev", "data"));
   757             body.append(");\n");
   758             body.append("}\n");
   759             
   760             functions.add(n);
   761             functions.add(n + "__VLjava_lang_Object_2Ljava_lang_Object_2");
   762         }
   763         return true;
   764     }
   765 
   766     private boolean generateOnChange(Element clazz, Map<String,Collection<String>> propDeps,
   767         Property[] properties, String className, 
   768         Map<String, Collection<String>> functionDeps
   769     ) {
   770         for (Element m : clazz.getEnclosedElements()) {
   771             if (m.getKind() != ElementKind.METHOD) {
   772                 continue;
   773             }
   774             ExecutableElement e = (ExecutableElement) m;
   775             OnPropertyChange onPC = e.getAnnotation(OnPropertyChange.class);
   776             if (onPC == null) {
   777                 continue;
   778             }
   779             for (String pn : onPC.value()) {
   780                 if (findProperty(properties, pn) == null && findDerivedFrom(propDeps, pn).isEmpty()) {
   781                     err().printMessage(Diagnostic.Kind.ERROR, "No property named '" + pn + "' in the model");
   782                     return false;
   783                 }
   784             }
   785             if (!e.getModifiers().contains(Modifier.STATIC)) {
   786                 err().printMessage(
   787                     Diagnostic.Kind.ERROR, "@OnPropertyChange method needs to be static", e);
   788                 return false;
   789             }
   790             if (e.getModifiers().contains(Modifier.PRIVATE)) {
   791                 err().printMessage(
   792                     Diagnostic.Kind.ERROR, "@OnPropertyChange method cannot be private", e);
   793                 return false;
   794             }
   795             if (e.getReturnType().getKind() != TypeKind.VOID) {
   796                 err().printMessage(
   797                     Diagnostic.Kind.ERROR, "@OnPropertyChange method should return void", e);
   798                 return false;
   799             }
   800             String n = e.getSimpleName().toString();
   801             
   802             
   803             for (String pn : onPC.value()) {
   804                 StringBuilder call = new StringBuilder();
   805                 call.append("  ").append(clazz.getSimpleName()).append(".").append(n).append("(");
   806                 call.append(wrapPropName(e, className, "name", pn));
   807                 call.append(");\n");
   808                 
   809                 Collection<String> change = functionDeps.get(pn);
   810                 if (change == null) {
   811                     change = new ArrayList<>();
   812                     functionDeps.put(pn, change);
   813                 }
   814                 change.add(call.toString());
   815                 for (String dpn : findDerivedFrom(propDeps, pn)) {
   816                     change = functionDeps.get(dpn);
   817                     if (change == null) {
   818                         change = new ArrayList<>();
   819                         functionDeps.put(dpn, change);
   820                     }
   821                     change.add(call.toString());
   822                 }
   823             }
   824         }
   825         return true;
   826     }
   827     
   828     private boolean generateReceive(
   829         Element clazz, StringWriter body, String className, 
   830         List<? extends Element> enclosedElements, List<String> functions
   831     ) {
   832         for (Element m : enclosedElements) {
   833             if (m.getKind() != ElementKind.METHOD) {
   834                 continue;
   835             }
   836             ExecutableElement e = (ExecutableElement)m;
   837             OnReceive onR = e.getAnnotation(OnReceive.class);
   838             if (onR == null) {
   839                 continue;
   840             }
   841             if (!e.getModifiers().contains(Modifier.STATIC)) {
   842                 err().printMessage(
   843                     Diagnostic.Kind.ERROR, "@OnReceive method needs to be static", e
   844                 );
   845                 return false;
   846             }
   847             if (e.getModifiers().contains(Modifier.PRIVATE)) {
   848                 err().printMessage(
   849                     Diagnostic.Kind.ERROR, "@OnReceive method cannot be private", e
   850                 );
   851                 return false;
   852             }
   853             if (e.getReturnType().getKind() != TypeKind.VOID) {
   854                 err().printMessage(
   855                     Diagnostic.Kind.ERROR, "@OnReceive method should return void", e
   856                 );
   857                 return false;
   858             }
   859             String modelClass = null;
   860             boolean expectsList = false;
   861             List<String> args = new ArrayList<>();
   862             {
   863                 for (VariableElement ve : e.getParameters()) {
   864                     TypeMirror modelType = null;
   865                     if (ve.asType().toString().equals(className)) {
   866                         args.add(className + ".this");
   867                     } else if (isModel(ve.asType())) {
   868                         modelType = ve.asType();
   869                     } else if (ve.asType().getKind() == TypeKind.ARRAY) {
   870                         modelType = ((ArrayType)ve.asType()).getComponentType();
   871                         expectsList = true;
   872                     }
   873                     if (modelType != null) {
   874                         if (modelClass != null) {
   875                             err().printMessage(Diagnostic.Kind.ERROR, "There can be only one model class among arguments", e);
   876                         } else {
   877                             modelClass = modelType.toString();
   878                             if (expectsList) {
   879                                 args.add("arr");
   880                             } else {
   881                                 args.add("arr[0]");
   882                             }
   883                         }
   884                     }
   885                 }
   886             }
   887             if (modelClass == null) {
   888                 err().printMessage(Diagnostic.Kind.ERROR, "The method needs to have one @Model class as parameter", e);
   889             }
   890             String n = e.getSimpleName().toString();
   891             body.append("public void ").append(n).append("(");
   892             StringBuilder assembleURL = new StringBuilder();
   893             {
   894                 String sep = "";
   895                 for (String p : findParamNames(e, onR.url(), assembleURL)) {
   896                     body.append(sep);
   897                     body.append("String ").append(p);
   898                     sep = ", ";
   899                 }
   900             }
   901             body.append(") {\n");
   902             body.append("  final Object[] result = { null };\n");
   903             body.append(
   904                 "  class ProcessResult implements Runnable {\n" +
   905                 "    @Override\n" +
   906                 "    public void run() {\n" +
   907                 "      Object value = result[0];\n");
   908             body.append(
   909                 "      " + modelClass + "[] arr;\n");
   910             body.append(
   911                 "      if (value instanceof Object[]) {\n" +
   912                 "        Object[] data = ((Object[])value);\n" +
   913                 "        arr = new " + modelClass + "[data.length];\n" +
   914                 "        for (int i = 0; i < data.length; i++) {\n" +
   915                 "          arr[i] = new " + modelClass + "(data[i]);\n" +
   916                 "        }\n" +
   917                 "      } else {\n" +
   918                 "        arr = new " + modelClass + "[1];\n" +
   919                 "        arr[0] = new " + modelClass + "(value);\n" +
   920                 "      }\n"
   921             );
   922             {
   923                 body.append(clazz.getSimpleName()).append(".").append(n).append("(");
   924                 String sep = "";
   925                 for (String arg : args) {
   926                     body.append(sep);
   927                     body.append(arg);
   928                     sep = ", ";
   929                 }
   930                 body.append(");\n");
   931             }
   932             body.append(
   933                 "    }\n" +
   934                 "  }\n"
   935             );
   936             body.append("  org.apidesign.bck2brwsr.htmlpage.ConvertTypes.loadJSON(\n      ");
   937             body.append(assembleURL);
   938             body.append(", result, new ProcessResult()\n  );\n");
   939 //            body.append("  ").append(clazz.getSimpleName()).append(".").append(n).append("(");
   940 //            body.append(wrapParams(e, null, className, "ev", "data"));
   941 //            body.append(");\n");
   942             body.append("}\n");
   943         }
   944         return true;
   945     }
   946 
   947     private CharSequence wrapParams(
   948         ExecutableElement ee, String id, String className, String evName, String dataName
   949     ) {
   950         TypeMirror stringType = processingEnv.getElementUtils().getTypeElement("java.lang.String").asType();
   951         StringBuilder params = new StringBuilder();
   952         boolean first = true;
   953         for (VariableElement ve : ee.getParameters()) {
   954             if (!first) {
   955                 params.append(", ");
   956             }
   957             first = false;
   958             String toCall = null;
   959             if (ve.asType() == stringType) {
   960                 if (ve.getSimpleName().contentEquals("id")) {
   961                     params.append('"').append(id).append('"');
   962                     continue;
   963                 }
   964                 toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toString(";
   965             }
   966             if (ve.asType().getKind() == TypeKind.DOUBLE) {
   967                 toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toDouble(";
   968             }
   969             if (ve.asType().getKind() == TypeKind.INT) {
   970                 toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toInt(";
   971             }
   972             if (dataName != null && ve.getSimpleName().contentEquals(dataName) && isModel(ve.asType())) {
   973                 toCall = "org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toModel(" + ve.asType() + ".class, ";
   974             }
   975 
   976             if (toCall != null) {
   977                 params.append(toCall);
   978                 if (dataName != null && ve.getSimpleName().contentEquals(dataName)) {
   979                     params.append(dataName);
   980                     params.append(", null");
   981                 } else {
   982                     if (evName == null) {
   983                         final StringBuilder sb = new StringBuilder();
   984                         sb.append("Unexpected string parameter name.");
   985                         if (dataName != null) {
   986                             sb.append(" Try \"").append(dataName).append("\"");
   987                         }
   988                         err().printMessage(Diagnostic.Kind.ERROR, sb.toString(), ee);
   989                     }
   990                     params.append(evName);
   991                     params.append(", \"");
   992                     params.append(ve.getSimpleName().toString());
   993                     params.append("\"");
   994                 }
   995                 params.append(")");
   996                 continue;
   997             }
   998             String rn = fqn(ve.asType(), ee);
   999             int last = rn.lastIndexOf('.');
  1000             if (last >= 0) {
  1001                 rn = rn.substring(last + 1);
  1002             }
  1003             if (rn.equals(className)) {
  1004                 params.append(className).append(".this");
  1005                 continue;
  1006             }
  1007             err().printMessage(Diagnostic.Kind.ERROR, 
  1008                 "@On method can only accept String named 'id' or " + className + " arguments",
  1009                 ee
  1010             );
  1011         }
  1012         return params;
  1013     }
  1014     
  1015     
  1016     private CharSequence wrapPropName(
  1017         ExecutableElement ee, String className, String propName, String propValue
  1018     ) {
  1019         TypeMirror stringType = processingEnv.getElementUtils().getTypeElement("java.lang.String").asType();
  1020         StringBuilder params = new StringBuilder();
  1021         boolean first = true;
  1022         for (VariableElement ve : ee.getParameters()) {
  1023             if (!first) {
  1024                 params.append(", ");
  1025             }
  1026             first = false;
  1027             if (ve.asType() == stringType) {
  1028                 if (propName != null && ve.getSimpleName().contentEquals(propName)) {
  1029                     params.append('"').append(propValue).append('"');
  1030                 } else {
  1031                     err().printMessage(Diagnostic.Kind.ERROR, "Unexpected string parameter name. Try \"" + propName + "\".");
  1032                 }
  1033                 continue;
  1034             }
  1035             String rn = fqn(ve.asType(), ee);
  1036             int last = rn.lastIndexOf('.');
  1037             if (last >= 0) {
  1038                 rn = rn.substring(last + 1);
  1039             }
  1040             if (rn.equals(className)) {
  1041                 params.append(className).append(".this");
  1042                 continue;
  1043             }
  1044             err().printMessage(Diagnostic.Kind.ERROR,
  1045                 "@OnPropertyChange method can only accept String or " + className + " arguments",
  1046                 ee);
  1047         }
  1048         return params;
  1049     }
  1050     
  1051     private boolean isModel(TypeMirror tm) {
  1052         final Element e = processingEnv.getTypeUtils().asElement(tm);
  1053         if (e == null) {
  1054             return false;
  1055         }
  1056         for (Element ch : e.getEnclosedElements()) {
  1057             if (ch.getKind() == ElementKind.METHOD) {
  1058                 ExecutableElement ee = (ExecutableElement)ch;
  1059                 if (ee.getParameters().isEmpty() && ee.getSimpleName().contentEquals("modelFor")) {
  1060                     return true;
  1061                 }
  1062             }
  1063         }
  1064         return models.values().contains(e.getSimpleName().toString());
  1065     }
  1066 
  1067     private void writeStringArray(List<String> strings, Writer w) throws IOException {
  1068         w.write("new String[] {\n");
  1069         String sep = "";
  1070         for (String n : strings) {
  1071             w.write(sep);
  1072             if (n == null) {
  1073                 w.write("    null");
  1074             } else {
  1075                 w.write("    \"" + n + "\"");
  1076             }
  1077             sep = ",\n";
  1078         }
  1079         w.write("\n  }");
  1080     }
  1081     
  1082     private void writeToString(Property[] props, Writer w) throws IOException {
  1083         w.write("  public String toString() {\n");
  1084         w.write("    StringBuilder sb = new StringBuilder();\n");
  1085         w.write("    sb.append('{');\n");
  1086         String sep = "";
  1087         for (Property p : props) {
  1088             w.write(sep);
  1089             w.append("    sb.append(\"" + p.name() + ": \");\n");
  1090             w.append("    sb.append(org.apidesign.bck2brwsr.htmlpage.ConvertTypes.toJSON(prop_");
  1091             w.append(p.name()).append("));\n");
  1092             sep =    "    sb.append(',');\n";
  1093         }
  1094         w.write("    sb.append('}');\n");
  1095         w.write("    return sb.toString();\n");
  1096         w.write("  }\n");
  1097     }
  1098     private void writeClone(String className, Property[] props, Writer w) throws IOException {
  1099         w.write("  public " + className + " clone() {\n");
  1100         w.write("    " + className + " ret = new " + className + "();\n");
  1101         for (Property p : props) {
  1102             if (!p.array()) {
  1103                 boolean isModel[] = { false };
  1104                 boolean isEnum[] = { false };
  1105                 checkType(p, isModel, isEnum);
  1106                 if (!isModel[0]) {
  1107                     w.write("    ret.prop_" + p.name() + " = prop_" + p.name() + ";\n");
  1108                     continue;
  1109                 }
  1110             }
  1111             w.write("    ret.prop_" + p.name() + " = prop_" + p.name() + ".clone();\n");
  1112         }
  1113         
  1114         w.write("    return ret;\n");
  1115         w.write("  }\n");
  1116     }
  1117 
  1118     private String inPckName(Element e) {
  1119         StringBuilder sb = new StringBuilder();
  1120         while (e.getKind() != ElementKind.PACKAGE) {
  1121             if (sb.length() == 0) {
  1122                 sb.append(e.getSimpleName());
  1123             } else {
  1124                 sb.insert(0, '.');
  1125                 sb.insert(0, e.getSimpleName());
  1126             }
  1127             e = e.getEnclosingElement();
  1128         }
  1129         return sb.toString();
  1130     }
  1131 
  1132     private String fqn(TypeMirror pt, Element relative) {
  1133         if (pt.getKind() == TypeKind.ERROR) {
  1134             final Elements eu = processingEnv.getElementUtils();
  1135             PackageElement pckg = eu.getPackageOf(relative);
  1136             return pckg.getQualifiedName() + "." + pt.toString();
  1137         }
  1138         return pt.toString();
  1139     }
  1140 
  1141     private String checkType(Property p, boolean[] isModel, boolean[] isEnum) {
  1142         String ret;
  1143         try {
  1144             ret = p.type().getName();
  1145         } catch (MirroredTypeException ex) {
  1146             TypeMirror tm = processingEnv.getTypeUtils().erasure(ex.getTypeMirror());
  1147             final Element e = processingEnv.getTypeUtils().asElement(tm);
  1148             final Model m = e == null ? null : e.getAnnotation(Model.class);
  1149             if (m != null) {
  1150                 ret = findPkgName(e) + '.' + m.className();
  1151                 isModel[0] = true;
  1152                 models.put(e, m.className());
  1153             } else {
  1154                 ret = tm.toString();
  1155             }
  1156             TypeMirror enm = processingEnv.getElementUtils().getTypeElement("java.lang.Enum").asType();
  1157             enm = processingEnv.getTypeUtils().erasure(enm);
  1158             isEnum[0] = processingEnv.getTypeUtils().isSubtype(tm, enm);
  1159         }
  1160         return ret;
  1161     }
  1162 
  1163     private Iterable<String> findParamNames(Element e, String url, StringBuilder assembleURL) {
  1164         List<String> params = new ArrayList<>();
  1165 
  1166         for (int pos = 0; ;) {
  1167             int next = url.indexOf('{', pos);
  1168             if (next == -1) {
  1169                 assembleURL.append('"')
  1170                     .append(url.substring(pos))
  1171                     .append('"');
  1172                 return params;
  1173             }
  1174             int close = url.indexOf('}', next);
  1175             if (close == -1) {
  1176                 err().printMessage(Diagnostic.Kind.ERROR, "Unbalanced '{' and '}' in " + url, e);
  1177                 return params;
  1178             }
  1179             final String paramName = url.substring(next + 1, close);
  1180             params.add(paramName);
  1181             assembleURL.append('"')
  1182                 .append(url.substring(pos, next))
  1183                 .append("\" + ").append(paramName).append(" + ");
  1184             pos = close + 1;
  1185         }
  1186     }
  1187 
  1188     private static Property findProperty(Property[] properties, String propName) {
  1189         for (Property p : properties) {
  1190             if (propName.equals(p.name())) {
  1191                 return p;
  1192             }
  1193         }
  1194         return null;
  1195     }
  1196 
  1197     private boolean isPrimitive(String type) {
  1198         return 
  1199             "int".equals(type) ||
  1200             "double".equals(type) ||
  1201             "long".equals(type) ||
  1202             "short".equals(type) ||
  1203             "byte".equals(type) ||
  1204             "float".equals(type);
  1205     }
  1206 
  1207     private static Collection<String> findDerivedFrom(Map<String, Collection<String>> propsDeps, String derivedProp) {
  1208         Set<String> names = new HashSet<>();
  1209         for (Map.Entry<String, Collection<String>> e : propsDeps.entrySet()) {
  1210             if (e.getValue().contains(derivedProp)) {
  1211                 names.add(e.getKey());
  1212             }
  1213         }
  1214         return names;
  1215     }
  1216 }