# HG changeset patch # User Jaroslav Tulach # Date 1365158597 -7200 # Node ID 19b4ddc302a68cadbf0b78e1f96100ff59226ad1 # Parent 0cb657a2b88877bbfe3d434b005d9e3457a6affd @OnReceive annotation can obtain and process single JSON object diff -r 0cb657a2b888 -r 19b4ddc302a6 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java Fri Apr 05 10:41:07 2013 +0200 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypes.java Fri Apr 05 12:43:17 2013 +0200 @@ -73,4 +73,26 @@ private static Object getProperty(Object object, String property) { return null; } + + @JavaScriptBody(args = { "url", "arr", "callback" }, body = "" + + "var request = new XMLHttpRequest();\n" + + "request.open('GET', url, true);\n" + + "request.setRequestHeader('Content-Type', 'application/json; charset=utf-8');\n" + + "request.onreadystatechange = function() {\n" + + " if (this.readyState!==4) return;\n" + + " arr[0] = eval('(' + this.response + ')');\n" + + " callback.run__V();\n" + + "};" + + "request.send();" + ) + public static native void loadJSON( + String url, Object[] jsonResult, Runnable whenDone + ); + + public static void extractJSON(Object jsonObject, String[] props, Object[] values) { + for (int i = 0; i < props.length; i++) { + values[i] = getProperty(jsonObject, props[i]); + } + } + } diff -r 0cb657a2b888 -r 19b4ddc302a6 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java --- a/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java Fri Apr 05 10:41:07 2013 +0200 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/PageProcessor.java Fri Apr 05 12:43:17 2013 +0200 @@ -34,6 +34,7 @@ import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Completion; import javax.annotation.processing.Completions; +import javax.annotation.processing.Messager; import javax.annotation.processing.Processor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; @@ -57,6 +58,7 @@ import org.apidesign.bck2brwsr.htmlpage.api.Model; import org.apidesign.bck2brwsr.htmlpage.api.On; import org.apidesign.bck2brwsr.htmlpage.api.OnFunction; +import org.apidesign.bck2brwsr.htmlpage.api.OnReceive; import org.apidesign.bck2brwsr.htmlpage.api.Page; import org.apidesign.bck2brwsr.htmlpage.api.Property; import org.openide.util.lookup.ServiceProvider; @@ -71,6 +73,7 @@ "org.apidesign.bck2brwsr.htmlpage.api.Model", "org.apidesign.bck2brwsr.htmlpage.api.Page", "org.apidesign.bck2brwsr.htmlpage.api.OnFunction", + "org.apidesign.bck2brwsr.htmlpage.api.OnReceive", "org.apidesign.bck2brwsr.htmlpage.api.On" }) public final class PageProcessor extends AbstractProcessor { @@ -103,6 +106,10 @@ return processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, pkg, name).openInputStream(); } } + + private Messager err() { + return processingEnv.getMessager(); + } private boolean processModel(Element e) { boolean ok = true; @@ -135,18 +142,64 @@ w.append("import org.apidesign.bck2brwsr.htmlpage.KOList;\n"); w.append("import org.apidesign.bck2brwsr.core.JavaScriptOnly;\n"); w.append("final class ").append(className).append(" implements Cloneable {\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(" private static Class<" + inPckName(e) + "> modelFor() { return null; }\n"); w.append(" public ").append(className).append("() {\n"); + w.append(" intKnckt();\n"); + w.append(" };\n"); + w.append(" private void intKnckt() {\n"); w.append(" ko = org.apidesign.bck2brwsr.htmlpage.Knockout.applyBindings(this, "); writeStringArray(propsGetSet, w); w.append(", "); writeStringArray(functions, w); w.append(" );\n"); w.append(" };\n"); + w.append(" ").append(className).append("(Object json) {\n"); + int values = 0; + for (int i = 0; i < propsGetSet.size(); i += 4) { + if (propsGetSet.get(i + 2) == null) { + continue; + } + values++; + } + w.append(" Object[] ret = new Object[" + values + "];\n"); + w.append(" org.apidesign.bck2brwsr.htmlpage.ConvertTypes.extractJSON(json, new String[] {\n"); + for (int i = 0; i < propsGetSet.size(); i += 4) { + if (propsGetSet.get(i + 2) == null) { + continue; + } + w.append(" \"").append(propsGetSet.get(i)).append("\",\n"); + } + w.append(" }, ret);\n"); + for (int i = 0, cnt = 0, prop = 0; i < propsGetSet.size(); i += 4) { + if (propsGetSet.get(i + 2) == null) { + continue; + } + boolean[] isModel = { false }; + boolean[] isEnum = { false }; + String type = checkType(m.properties()[prop++], isModel, isEnum); + w.append(" this.prop_").append(propsGetSet.get(i)).append(" = "); + boolean close = false; + if (isEnum[0]) { +// w.append(type).append(".valueOf((String)"); +// close = true; + w.append("null;\n"); + continue; + } else { + w.append('(').append(type).append(')'); + } + w.append("ret[" + cnt++ + "]"); + if (close) { + w.append(");\n"); + } else { + w.append(";\n"); + } + + } + w.append(" intKnckt();\n"); + w.append(" };\n"); writeToString(m.properties(), w); writeClone(className, m.properties(), w); w.append("}\n"); @@ -154,7 +207,7 @@ w.close(); } } catch (IOException ex) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e); + err().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e); return false; } return ok; @@ -173,7 +226,7 @@ pp = ProcessPage.readPage(is); is.close(); } catch (IOException iOException) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Can't read " + p.xhtml() + " as " + iOException.getMessage(), e); + err().printMessage(Diagnostic.Kind.ERROR, "Can't read " + p.xhtml() + " as " + iOException.getMessage(), e); ok = false; pp = null; } @@ -197,6 +250,9 @@ if (!generateFunctions(e, body, className, e.getEnclosedElements(), functions)) { ok = false; } + if (!generateReceive(e, body, className, e.getEnclosedElements(), functions)) { + ok = false; + } FileObject java = processingEnv.getFiler().createSourceFile(pkg + '.' + className, e); w = new OutputStreamWriter(java.openOutputStream()); @@ -237,7 +293,7 @@ w.close(); } } catch (IOException ex) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e); + err().printMessage(Diagnostic.Kind.ERROR, "Can't create " + className + ".java", e); return false; } return ok; @@ -283,24 +339,24 @@ if (oc != null) { for (String id : oc.id()) { if (pp == null) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "id = " + id + " not found in HTML page."); + err().printMessage(Diagnostic.Kind.ERROR, "id = " + id + " not found in HTML page."); ok = false; continue; } if (pp.tagNameForId(id) == null) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "id = " + id + " does not exist in the HTML page. Found only " + pp.ids(), method); + err().printMessage(Diagnostic.Kind.ERROR, "id = " + id + " does not exist in the HTML page. Found only " + pp.ids(), method); ok = false; continue; } ExecutableElement ee = (ExecutableElement)method; CharSequence params = wrapParams(ee, id, className, "ev", null); if (!ee.getModifiers().contains(Modifier.STATIC)) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@On method has to be static", ee); + err().printMessage(Diagnostic.Kind.ERROR, "@On method has to be static", ee); ok = false; continue; } if (ee.getModifiers().contains(Modifier.PRIVATE)) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@On method can't be private", ee); + err().printMessage(Diagnostic.Kind.ERROR, "@On method can't be private", ee); ok = false; continue; } @@ -553,7 +609,7 @@ if (!isModel[0] && !"java.lang.String".equals(ret) && !isEnum[0]) { String bt = findBoxedType(ret); if (bt == null) { - processingEnv.getMessager().printMessage( + err().printMessage( Diagnostic.Kind.ERROR, "Only primitive types supported in the mapping. Not " + ret, where @@ -604,7 +660,7 @@ sb.append('"'); sep = ", "; } - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, + err().printMessage(Diagnostic.Kind.ERROR, propName + " is not one of known properties: " + sb , e ); @@ -634,19 +690,19 @@ continue; } if (!e.getModifiers().contains(Modifier.STATIC)) { - processingEnv.getMessager().printMessage( + err().printMessage( Diagnostic.Kind.ERROR, "@OnFunction method needs to be static", e ); return false; } if (e.getModifiers().contains(Modifier.PRIVATE)) { - processingEnv.getMessager().printMessage( + err().printMessage( Diagnostic.Kind.ERROR, "@OnFunction method cannot be private", e ); return false; } if (e.getReturnType().getKind() != TypeKind.VOID) { - processingEnv.getMessager().printMessage( + err().printMessage( Diagnostic.Kind.ERROR, "@OnFunction method should return void", e ); return false; @@ -664,6 +720,100 @@ return true; } + private boolean generateReceive( + Element clazz, StringWriter body, String className, + List enclosedElements, List functions + ) { + for (Element m : enclosedElements) { + if (m.getKind() != ElementKind.METHOD) { + continue; + } + ExecutableElement e = (ExecutableElement)m; + OnReceive onR = e.getAnnotation(OnReceive.class); + if (onR == null) { + continue; + } + if (!e.getModifiers().contains(Modifier.STATIC)) { + err().printMessage( + Diagnostic.Kind.ERROR, "@OnReceive method needs to be static", e + ); + return false; + } + if (e.getModifiers().contains(Modifier.PRIVATE)) { + err().printMessage( + Diagnostic.Kind.ERROR, "@OnReceive method cannot be private", e + ); + return false; + } + if (e.getReturnType().getKind() != TypeKind.VOID) { + err().printMessage( + Diagnostic.Kind.ERROR, "@OnReceive method should return void", e + ); + return false; + } + String modelClass = null; + List args = new ArrayList<>(); + { + for (VariableElement ve : e.getParameters()) { + if (ve.asType().toString().equals(className)) { + args.add(className + ".this"); + } else if (isModel(ve.asType())) { + if (modelClass != null) { + err().printMessage(Diagnostic.Kind.ERROR, "There can be only one model class among arguments", e); + } else { + modelClass = ve.asType().toString(); + args.add("new " + modelClass + "(value)"); + } + } + } + } + String n = e.getSimpleName().toString(); + body.append("public void ").append(n).append("("); + StringBuilder assembleURL = new StringBuilder(); + { + String sep = ""; + for (String p : findParamNames(e, onR.url(), assembleURL)) { + body.append(sep); + body.append("String ").append(p); + sep = ", "; + } + } + body.append(") {\n"); + body.append(" final Object[] result = { null };\n"); + body.append( + " class ProcessResult implements Runnable {\n" + + " @Override\n" + + " public void run() {\n" + + " Object value = result[0];\n" + + " if (value instanceof Object[]) {\n" + + " throw new IllegalStateException(\"Array value: \" + value);\n" + + " } else {\n "); + { + body.append(clazz.getSimpleName()).append(".").append(n).append("("); + String sep = ""; + for (String arg : args) { + body.append(sep); + body.append(arg); + sep = ", "; + } + body.append(");\n"); + } + body.append( + " }\n" + + " }\n" + + " }\n" + ); + body.append(" org.apidesign.bck2brwsr.htmlpage.ConvertTypes.loadJSON(\n "); + body.append(assembleURL); + body.append(", result, new ProcessResult()\n );\n"); +// body.append(" ").append(clazz.getSimpleName()).append(".").append(n).append("("); +// body.append(wrapParams(e, null, className, "ev", "data")); +// body.append(");\n"); + body.append("}\n"); + } + return true; + } + private CharSequence wrapParams( ExecutableElement ee, String id, String className, String evName, String dataName ) { @@ -716,7 +866,7 @@ params.append(className).append(".this"); continue; } - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, + err().printMessage(Diagnostic.Kind.ERROR, "@On method can only accept String named 'id' or " + className + " arguments", ee ); @@ -835,4 +985,29 @@ } return ret; } + + private Iterable findParamNames(Element e, String url, StringBuilder assembleURL) { + List params = new ArrayList<>(); + + for (int pos = 0; ;) { + int next = url.indexOf('{', pos); + if (next == -1) { + assembleURL.append('"') + .append(url.substring(pos)) + .append('"'); + return params; + } + int close = url.indexOf('}', next); + if (close == -1) { + err().printMessage(Diagnostic.Kind.ERROR, "Unbalanced '{' and '}' in " + url, e); + return params; + } + final String paramName = url.substring(next + 1, close); + params.add(paramName); + assembleURL.append('"') + .append(url.substring(pos, next)) + .append("\" + ").append(paramName).append(" + "); + pos = close + 1; + } + } } diff -r 0cb657a2b888 -r 19b4ddc302a6 javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/OnReceive.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/api/src/main/java/org/apidesign/bck2brwsr/htmlpage/api/OnReceive.java Fri Apr 05 12:43:17 2013 +0200 @@ -0,0 +1,41 @@ +/** + * 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; + +/** Static methods in classes annotated by {@link Model} or {@link Page} + * can be marked by this annotation establish a JSON communication point. + * The associated model page then gets new method to invoke a network + * connection + * + * @author Jaroslav Tulach + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface OnReceive { + /** The URL to connect to. Can contain variable names surrounded by '{' and '}'. + * Those parameters will then become variables of the associated method. + * + * @return the (possibly parametrized) url to connect to + */ + String url(); +} diff -r 0cb657a2b888 -r 19b4ddc302a6 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypesTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/ConvertTypesTest.java Fri Apr 05 12:43:17 2013 +0200 @@ -0,0 +1,52 @@ +/** + * 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.core.JavaScriptBody; +import org.apidesign.bck2brwsr.vmtest.BrwsrTest; +import org.apidesign.bck2brwsr.vmtest.VMTest; +import org.testng.annotations.Factory; + +/** + * + * @author Jaroslav Tulach + */ +public class ConvertTypesTest { + @JavaScriptBody(args = { }, body = "var json = new Object();" + + "json.firstName = 'son';\n" + + "json.lastName = 'dj';\n" + + "json.sex = 'MALE';\n" + + "return json;" + ) + private static native Object createJSON(); + + @BrwsrTest + public void testConvertToPeople() { + final Object o = createJSON(); + + Person p = new Person(o); + + assert "son".equals(p.getFirstName()) : "First name: " + p.getFirstName(); + assert "dj".equals(p.getLastName()) : "Last name: " + p.getLastName(); +// assert Sex.MALE.equals(p.getSex()) : "Sex: " + p.getSex(); + } + + @Factory public static Object[] create() { + return VMTest.create(ConvertTypesTest.class); + } +} \ No newline at end of file diff -r 0cb657a2b888 -r 19b4ddc302a6 javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/JSONTest.java --- a/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/JSONTest.java Fri Apr 05 10:41:07 2013 +0200 +++ b/javaquery/api/src/test/java/org/apidesign/bck2brwsr/htmlpage/JSONTest.java Fri Apr 05 12:43:17 2013 +0200 @@ -18,16 +18,26 @@ package org.apidesign.bck2brwsr.htmlpage; import java.util.Iterator; +import org.apidesign.bck2brwsr.htmlpage.api.OnReceive; +import org.apidesign.bck2brwsr.htmlpage.api.Page; +import org.apidesign.bck2brwsr.htmlpage.api.Property; +import org.apidesign.bck2brwsr.vmtest.BrwsrTest; +import org.apidesign.bck2brwsr.vmtest.Http; +import org.apidesign.bck2brwsr.vmtest.VMTest; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import org.testng.annotations.Test; import static org.testng.Assert.*; +import org.testng.annotations.Factory; /** Need to verify that models produce reasonable JSON objects. * * @author Jaroslav Tulach */ +@Page(xhtml = "Empty.html", className = "JSONik", properties = { + @Property(name = "fetched", type = PersonImpl.class) +}) public class JSONTest { @Test public void personToString() throws JSONException { @@ -108,4 +118,60 @@ assertEquals(o.getJSONArray("nicknames").getString(1), n2); assertEquals(o.getJSONArray("age").getInt(1), 73); } + + + @OnReceive(url="/{url}") + static void fetch(Person p, JSONik model) { + model.setFetched(p); + throw new IllegalStateException("Got him: " + p); + } + + @Http(@Http.Resource( + content = "{'firstName': 'Sitar', 'sex': 'MALE'}", + path="/person.json", + mimeType = "application/json" + )) + @BrwsrTest public void loadAndParseJSON() { + JSONik js = new JSONik(); + js.applyBindings(); + + js.fetch("person.json"); + + Person p = null; + for (int i = 0; i < 10000000; i++) { + if (js.getFetched() != null) { + p = js.getFetched(); + } + } + assert p != null : "We should get our person back: " + p; + assert "Sitar".equals(p.getFirstName()) : "Expecting Sitar: " + p.getFirstName(); + assert Sex.MALE.equals(p.getSex()) : "Expecting MALE: " + p.getSex(); + } + + @Http(@Http.Resource( + content = "[{'firstName': 'Sitar', 'sex': 'MALE'}]", + path="/person.json", + mimeType = "application/json" + )) + @BrwsrTest public void loadAndParseJSONArray() { + JSONik js = new JSONik(); + js.applyBindings(); + + js.fetch("person.json"); + + Person p = null; + for (int i = 0; i < 10000000; i++) { + if (js.getFetched() != null) { + p = js.getFetched(); + } + } + assert p != null : "We should get our person back: " + p; + assert "Sitar".equals(p.getFirstName()) : "Expecting Sitar: " + p.getFirstName(); + assert Sex.MALE.equals(p.getSex()) : "Expecting MALE: " + p.getSex(); + } + + @Factory public static Object[] create() { + return VMTest.create(JSONTest.class); + } + }