# HG changeset patch # User Jaroslav Tulach # Date 1361519980 -3600 # Node ID 26513bd377b93f112098aa232956d945c484621e # Parent 0c0fe97fe0c7ccf6a47dc75b905e816b4d4a8e78 Introducing @Model for complex record-like types diff -r 0c0fe97fe0c7 -r 26513bd377b9 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java Wed Feb 20 18:14:59 2013 +0100 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java Fri Feb 22 08:59:40 2013 +0100 @@ -20,7 +20,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; +import java.io.StringWriter; import java.io.Writer; +import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -37,6 +39,7 @@ import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; @@ -51,6 +54,7 @@ import javax.tools.FileObject; import javax.tools.StandardLocation; import org.apidesign.bck2brwsr.htmlpage.api.ComputedProperty; +import org.apidesign.bck2brwsr.htmlpage.api.Model; import org.apidesign.bck2brwsr.htmlpage.api.On; import org.apidesign.bck2brwsr.htmlpage.api.Page; import org.apidesign.bck2brwsr.htmlpage.api.Property; @@ -63,6 +67,7 @@ */ @ServiceProvider(service=Processor.class) @SupportedAnnotationTypes({ + "org.apidesign.bck2brwsr.htmlpage.api.Model", "org.apidesign.bck2brwsr.htmlpage.api.Page", "org.apidesign.bck2brwsr.htmlpage.api.On" }) @@ -70,85 +75,14 @@ @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { boolean ok = true; + for (Element e : roundEnv.getElementsAnnotatedWith(Model.class)) { + if (!processModel(e)) { + ok = false; + } + } for (Element e : roundEnv.getElementsAnnotatedWith(Page.class)) { - Page p = e.getAnnotation(Page.class); - if (p == null) { - continue; - } - PackageElement pe = (PackageElement)e.getEnclosingElement(); - String pkg = pe.getQualifiedName().toString(); - - ProcessPage pp; - try { - InputStream is = openStream(pkg, p.xhtml()); - pp = ProcessPage.readPage(is); - is.close(); - } catch (IOException iOException) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Can't read " + p.xhtml(), e); + if (!processPage(e)) { ok = false; - pp = null; - } - Writer w; - String className = p.className(); - if (className.isEmpty()) { - int indx = p.xhtml().indexOf('.'); - className = p.xhtml().substring(0, indx); - } - try { - FileObject java = processingEnv.getFiler().createSourceFile(pkg + '.' + className, e); - w = new OutputStreamWriter(java.openOutputStream()); - try { - w.append("package " + pkg + ";\n"); - w.append("import org.apidesign.bck2brwsr.htmlpage.api.*;\n"); - w.append("import org.apidesign.bck2brwsr.htmlpage.KOList;\n"); - w.append("final class ").append(className).append(" {\n"); - w.append(" private boolean locked;\n"); - if (!initializeOnClick(className, (TypeElement) e, w, pp)) { - ok = false; - } else { - for (String id : pp.ids()) { - String tag = pp.tagNameForId(id); - String type = type(tag); - w.append(" ").append("public final "). - append(type).append(' ').append(cnstnt(id)).append(" = new "). - append(type).append("(\"").append(id).append("\");\n"); - } - } - List propsGetSet = new ArrayList(); - Map> propsDeps = new HashMap>(); - if (!generateComputedProperties(w, p.properties(), e.getEnclosedElements(), propsGetSet, propsDeps)) { - ok = false; - } - generateProperties(w, p.properties(), propsGetSet, propsDeps); - w.append(" private org.apidesign.bck2brwsr.htmlpage.Knockout ko;\n"); - if (!propsGetSet.isEmpty()) { - w.write("public " + className + " applyBindings() {\n"); - w.write(" ko = org.apidesign.bck2brwsr.htmlpage.Knockout.applyBindings("); - w.write(className + ".class, this, "); - w.write("new String[] {\n"); - String sep = ""; - for (String n : propsGetSet) { - w.write(sep); - if (n == null) { - w.write(" null"); - } else { - w.write(" \"" + n + "\""); - } - sep = ",\n"; - } - w.write("\n });\n return this;\n}\n"); - - w.write("public void triggerEvent(Element e, OnEvent ev) {\n"); - w.write(" org.apidesign.bck2brwsr.htmlpage.Knockout.triggerEvent(e.getId(), ev.getElementPropertyName());\n"); - w.write("}\n"); - } - w.append("}\n"); - } finally { - w.close(); - } - } catch (IOException ex) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e); - return false; } } return ok; @@ -163,6 +97,134 @@ return processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, pkg, name).openInputStream(); } } + + private boolean processModel(Element e) { + boolean ok = true; + Model m = e.getAnnotation(Model.class); + if (m == null) { + return true; + } + String pkg = findPkgName(e); + Writer w; + String className = m.className(); + try { + StringWriter body = new StringWriter(); + List propsGetSet = new ArrayList<>(); + Map> propsDeps = new HashMap<>(); + if (!generateComputedProperties(body, m.properties(), e.getEnclosedElements(), propsGetSet, propsDeps)) { + ok = false; + } + if (!generateProperties(body, m.properties(), propsGetSet, propsDeps)) { + ok = false; + } + FileObject java = processingEnv.getFiler().createSourceFile(pkg + '.' + className, e); + w = new OutputStreamWriter(java.openOutputStream()); + try { + w.append("package " + pkg + ";\n"); + w.append("import org.apidesign.bck2brwsr.htmlpage.api.*;\n"); + w.append("import org.apidesign.bck2brwsr.htmlpage.KOList;\n"); + w.append("final class ").append(className).append(" {\n"); + w.append(" private Object json;\n"); + w.append(" private boolean locked;\n"); + w.append(" private org.apidesign.bck2brwsr.htmlpage.Knockout ko;\n"); + w.append(body.toString()); + w.append("}\n"); + } finally { + w.close(); + } + } catch (IOException ex) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e); + return false; + } + return ok; + } + + private boolean processPage(Element e) { + boolean ok = true; + Page p = e.getAnnotation(Page.class); + if (p == null) { + return true; + } + String pkg = findPkgName(e); + + ProcessPage pp; + try (InputStream is = openStream(pkg, p.xhtml())) { + pp = ProcessPage.readPage(is); + is.close(); + } catch (IOException iOException) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Can't read " + p.xhtml(), e); + ok = false; + pp = null; + } + Writer w; + String className = p.className(); + if (className.isEmpty()) { + int indx = p.xhtml().indexOf('.'); + className = p.xhtml().substring(0, indx); + } + try { + StringWriter body = new StringWriter(); + List propsGetSet = new ArrayList<>(); + Map> propsDeps = new HashMap<>(); + if (!generateComputedProperties(body, p.properties(), e.getEnclosedElements(), propsGetSet, propsDeps)) { + ok = false; + } + if (!generateProperties(body, p.properties(), propsGetSet, propsDeps)) { + ok = false; + } + + FileObject java = processingEnv.getFiler().createSourceFile(pkg + '.' + className, e); + w = new OutputStreamWriter(java.openOutputStream()); + try { + w.append("package " + pkg + ";\n"); + w.append("import org.apidesign.bck2brwsr.htmlpage.api.*;\n"); + w.append("import org.apidesign.bck2brwsr.htmlpage.KOList;\n"); + w.append("final class ").append(className).append(" {\n"); + w.append(" private boolean locked;\n"); + if (!initializeOnClick(className, (TypeElement) e, w, pp)) { + ok = false; + } else { + for (String id : pp.ids()) { + String tag = pp.tagNameForId(id); + String type = type(tag); + w.append(" ").append("public final "). + append(type).append(' ').append(cnstnt(id)).append(" = new "). + append(type).append("(\"").append(id).append("\");\n"); + } + } + w.append(" private org.apidesign.bck2brwsr.htmlpage.Knockout ko;\n"); + w.append(body.toString()); + if (!propsGetSet.isEmpty()) { + w.write("public " + className + " applyBindings() {\n"); + w.write(" ko = org.apidesign.bck2brwsr.htmlpage.Knockout.applyBindings("); + w.write(className + ".class, this, "); + w.write("new String[] {\n"); + String sep = ""; + for (String n : propsGetSet) { + w.write(sep); + if (n == null) { + w.write(" null"); + } else { + w.write(" \"" + n + "\""); + } + sep = ",\n"; + } + w.write("\n });\n return this;\n}\n"); + + w.write("public void triggerEvent(Element e, OnEvent ev) {\n"); + w.write(" org.apidesign.bck2brwsr.htmlpage.Knockout.triggerEvent(e.getId(), ev.getElementPropertyName());\n"); + w.write("}\n"); + } + w.append("}\n"); + } finally { + w.close(); + } + } catch (IOException ex) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e); + return false; + } + return ok; + } private static String type(String tag) { if (tag.equals("title")) { @@ -295,8 +357,7 @@ Element cls = findClass(element); Page p = cls.getAnnotation(Page.class); - PackageElement pe = (PackageElement) cls.getEnclosingElement(); - String pkg = pe.getQualifiedName().toString(); + String pkg = findPkgName(cls); ProcessPage pp; try { InputStream is = openStream(pkg, p.xhtml()); @@ -306,7 +367,7 @@ return Collections.emptyList(); } - List cc = new ArrayList(); + List cc = new ArrayList<>(); userText = userText.substring(1); for (String id : pp.ids()) { if (id.startsWith(userText)) { @@ -327,12 +388,14 @@ return e.getEnclosingElement(); } - private void generateProperties( - Writer w, Property[] properties, Collection props, - Map> deps + private boolean generateProperties( + Writer w, Property[] properties, + Collection props, Map> deps ) throws IOException { + boolean ok = true; for (Property p : properties) { - final String tn = typeName(p); + final String tn; + tn = typeName(p); String[] gs = toGetSet(p.name(), tn, p.array()); if (p.array()) { @@ -382,6 +445,7 @@ props.add(gs[3]); props.add(gs[0]); } + return ok; } private boolean generateComputedProperties( @@ -427,7 +491,7 @@ Collection depends = deps.get(dn); if (depends == null) { - depends = new LinkedHashSet(); + depends = new LinkedHashSet<>(); deps.put(dn, depends); } depends.add(sn); @@ -494,11 +558,19 @@ private String typeName(Property p) { String ret; + boolean isModel = false; try { ret = p.type().getName(); } catch (MirroredTypeException ex) { TypeMirror tm = processingEnv.getTypeUtils().erasure(ex.getTypeMirror()); - ret = tm.toString(); + final Element e = processingEnv.getTypeUtils().asElement(tm); + final Model m = e == null ? null : e.getAnnotation(Model.class); + if (m != null) { + ret = findPkgName(e) + '.' + m.className(); + isModel = true; + } else { + ret = tm.toString(); + } } if (p.array()) { String bt = findBoxedType(ret); @@ -506,14 +578,12 @@ return bt; } } - if ("java.lang.String".equals(ret)) { - return ret; + if (!isModel && !"java.lang.String".equals(ret)) { + String bt = findBoxedType(ret); + if (bt == null) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Only primitive types supported in the mapping. Not " + ret); + } } - String bt = findBoxedType(ret); - if (bt != null) { - return ret; - } - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Only primitive types supported in the mapping. Not " + ret); return ret; } @@ -561,4 +631,13 @@ ); return false; } + + private static String findPkgName(Element e) { + for (;;) { + if (e.getKind() == ElementKind.PACKAGE) { + return ((PackageElement)e).getQualifiedName().toString(); + } + e = e.getEnclosingElement(); + } + } } diff -r 0c0fe97fe0c7 -r 26513bd377b9 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/Model.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/Model.java Fri Feb 22 08:59:40 2013 +0100 @@ -0,0 +1,43 @@ +/** + * Back 2 Browser Bytecode Translator + * Copyright (C) 2012 Jaroslav Tulach + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. Look for COPYING file in the top folder. + * If not, see http://opensource.org/licenses/GPL-2.0. + */ +package org.apidesign.bck2brwsr.htmlpage.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Defines a model class named {@link #className()} which contains + * defined {@link #properties()}. This class can have methods + * annotated by {@link ComputedProperty} which define derived + * properties in the model class. + *

+ * The {@link #className() generated class} will have methods + * to convert the object toJSON and fromJSON. + * + * @author Jaroslav Tulach + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.TYPE) +public @interface Model { + /** Name of the model class */ + String className(); + /** List of properties in the model. + */ + Property[] properties(); +} diff -r 0c0fe97fe0c7 -r 26513bd377b9 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/Property.java --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/Property.java Wed Feb 20 18:14:59 2013 +0100 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/Property.java Fri Feb 22 08:59:40 2013 +0100 @@ -20,16 +20,36 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.List; -/** Represents a property in a generated model of an HTML - * {@link Page}. +/** Represents a property. Either in a generated model of an HTML + * {@link Page} or in a class defined by {@link Model}. * * @author Jaroslav Tulach */ @Retention(RetentionPolicy.SOURCE) @Target({}) public @interface Property { + /** Name of the property. Will be used to define proper getter and setter + * in the associated class. + * + * @return valid java identifier + */ String name(); + + /** Type of the property. Can either be primitive type (like int.class, + * double.class, etc.), {@link String} or complex model + * class (defined by {@link Model} property). + * + * @return the class of the property + */ Class type(); + + /** Is this property an array of the {@link #type()} or a single value? + * If the property is an array, only its getter (returning mutable {@link List} of + * the boxed {@link #type()}). + * + * @return true, if this is supposed to be an array of values. + */ boolean array() default false; } diff -r 0c0fe97fe0c7 -r 26513bd377b9 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java --- a/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java Wed Feb 20 18:14:59 2013 +0100 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ModelTest.java Fri Feb 22 08:59:40 2013 +0100 @@ -33,20 +33,21 @@ * * @author Jaroslav Tulach */ -@Page(xhtml = "Empty.html", className = "Model", properties = { +@Page(xhtml = "Empty.html", className = "Modelik", properties = { @Property(name = "value", type = int.class), @Property(name = "count", type = int.class), @Property(name = "unrelated", type = long.class), @Property(name = "names", type = String.class, array = true), - @Property(name = "values", type = int.class, array = true) + @Property(name = "values", type = int.class, array = true), + @Property(name = "people", type = PersonImpl.class, array = true) }) public class ModelTest { - private Model model; - private static Model leakedModel; + private Modelik model; + private static Modelik leakedModel; @BeforeMethod public void createModel() { - model = new Model(); + model = new Modelik(); } @Test public void classGeneratedWithSetterGetter() { @@ -189,11 +190,21 @@ } static class MockKnockout extends Knockout { - List mutated = new ArrayList(); + List mutated = new ArrayList<>(); @Override public void valueHasMutated(String prop) { mutated.add(prop); } } + + public @Test void hasPersonPropertyAndComputedFullName() { + List arr = model.getPeople(); + assertEquals(arr.size(), 0, "By default empty"); + Person p = null; + if (p != null) { + String fullNameGenerated = p.getFullName(); + assertNotNull(fullNameGenerated); + } + } } diff -r 0c0fe97fe0c7 -r 26513bd377b9 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/PersonImpl.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/PersonImpl.java Fri Feb 22 08:59:40 2013 +0100 @@ -0,0 +1,37 @@ +/** + * Back 2 Browser Bytecode Translator + * Copyright (C) 2012 Jaroslav Tulach + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. Look for COPYING file in the top folder. + * If not, see http://opensource.org/licenses/GPL-2.0. + */ +package org.apidesign.bck2brwsr.htmlpage; + +import org.apidesign.bck2brwsr.htmlpage.api.ComputedProperty; +import org.apidesign.bck2brwsr.htmlpage.api.Model; +import org.apidesign.bck2brwsr.htmlpage.api.Property; + +/** + * + * @author Jaroslav Tulach + */ +@Model(className = "Person", properties = { + @Property(name = "firstName", type = String.class), + @Property(name = "lastName", type = String.class) +}) +final class PersonImpl { + @ComputedProperty + public static String fullName(String firstName, String lastName) { + return firstName + " " + lastName; + } +}