
net.java.html.json.Model Maven / Gradle / Ivy
Show all versions of net.java.html.json Show documentation
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package net.java.html.json;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URL;
import java.util.List;
/** Defines a model class that represents a single
* JSON-like object
* named {@link #className()}. The generated class contains
* getters and setters for properties defined via {@link #properties()} and
* getters for other, derived properties defined by annotating methods
* of this class by {@link ComputedProperty}. Each property
* can be of primitive type, an {@link Enum enum type} or (in order to create
* nested JSON structure)
* of another {@link Model class generated by @Model} annotation. Each property
* can either represent a single value or be an array of its values.
*
* The {@link #className() generated class}'s toString
method
* converts the state of the object into
* JSON format. One can
* use {@link Models#parse(net.java.html.BrwsrCtx, java.lang.Class, java.io.InputStream)}
* method to read the JSON text stored in a file or other stream back into the Java model.
* One can also use {@link OnReceive @OnReceive} annotation to read the model
* asynchronously from a {@link URL}.
*
* An example where one defines class Person
with four
* properties (firstName
, lastName
, array of addresses
and
* fullName
) follows:
*
* {@link Model @Model}(className="Person", properties={
* {@link Property @Property}(name = "firstName", type=String.class),
* {@link Property @Property}(name = "lastName", type=String.class)
* {@link Property @Property}(name = "addresses", type=Address.class, array = true)
* })
* static class PersonModel {
* {@link ComputedProperty @ComputedProperty}
* static String fullName(String firstName, String lastName) {
* return firstName + " " + lastName;
* }
*
* {@link ComputedProperty @ComputedProperty}
* static String mainAddress({@link List List<Address>} addresses) {
* for (Address a : addresses) {
* return a.getStreet() + " " + a.getTown();
* }
* return "No address";
* }
*
* {@link Model @Model}(className="Address", properties={
* {@link Property @Property}(name = "street", type=String.class),
* {@link Property @Property}(name = "town", type=String.class)
* })
* static class AddressModel {
* }
* }
*
* The generated model class has a default constructor, and also quick
* instantiation constructor that accepts all non-array properties
* (in the order used in the {@link #properties()} attribute) and vararg list
* for the first array property (if any). One can thus use following code
* to create an instance of the Person and Address classes:
*
* Person p = new Person("Jaroslav", "Tulach",
* new Address("Markoušovice", "Úpice"),
* new Address("V Parku", "Praha")
* );
* // p.toString() then returns equivalent of following JSON object
* {
* "firstName" : "Jaroslav",
* "lastName" : "Tulach",
* "addresses" : [
* { "street" : "Markoušovice", "town" : "Úpice" },
* { "street" : "V Parku", "town" : "Praha" },
* ]
* }
*
*
* In case you are using Knockout technology
* for Java then you can associate such model object with surrounding HTML page by
* calling: p.applyBindings();
(in case you specify {@link #targetId()}.
* The page can then use regular
* Knockout bindings to reference your
* model and create dynamic connection between your model in Java and
* live DOM structure in the browser:
*
*
* Name: <span data-bind="text: fullName">
* <div data-bind="foreach: addresses">
* Lives in <span data-bind="text: town"/>
* </div>
*
*
*
* Access Raw Knockout Observables
*
*
*
* One can obtain raw JavaScript object representing the
* instance of {@link Model model class} (with appropriate
* Knockout observable properties)
* by calling {@link Models#toRaw(java.lang.Object) Models.toRaw(p)}. For
* example here is a way to obtain the value of fullName
property
* (inefficient as it switches between Java and JavaScript back and forth,
* but functional and instructive) via a JavaScript call:
*
*
* {@link net.java.html.js.JavaScriptBody @JavaScriptBody}(args = "raw", javacall = true, body =
* "return raw.fullName();" // yes, the Knockout property is a function
* )
* static native String jsFullName(Object raw);
* // and later
* Person p = ...;
* String fullName = jsFullName({@link Models#toRaw(java.lang.Object) Models.toRaw(p)});
*
*
* The above shows how to read a value from Knockout
* observable. There is a way to change the value too:
* One can pass a parameter to the property-function and then
* it acts like a setter (of course not in the case of read only fullName
property,
* but for firstName
or lastName
the setter is
* available). Everything mentioned in this paragraph applies only when
* Knockout technology is active
* other technologies may behave differently.
*
*
*
* Copy to Plain JSON
*
*
*
* There is a way to pass a value of a Java {@link Model model class} instance
* by copy and convert
* the {@link Model the whole object} into plain
* JSON. Just
* print it as a string and parse it in JavaScript:
*
*
* {@link net.java.html.js.JavaScriptBody @JavaScriptBody}(args = { "txt" }, body =
* "return JSON.parse(txt);"
* )
* private static native Object parseJSON(String txt);
*
* public static Object toPlainJSON(Object model) {
* return parseJSON(model.toString());
* }
*
*
* The newly returned instance is a one time copy of the original model and is no longer
* connected to it. The copy based behavior is independent on any
* particular technology and should work
* in Knockout as well as other
* technology implementations.
*
*
*
* References
*
*
* Visit an on-line demo
* to see a histogram driven by the {@link Model} annotation or try
* a little math test.
*
* @author Jaroslav Tulach
*/
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Model {
/** Name of the model class.
* @return valid Java identifier to use as a name of the model class
*/
String className();
/** List of properties in the model.
* @return array of property definitions
*/
Property[] properties();
/** The id of an element to bind this model too. If this
* property is specified an applyBindings
method
* in the model class is going to be generated which later calls
* {@link Models#applyBindings(java.lang.Object, java.lang.String)}
* with appropriate targetId
. If the targetId
* is specified as empty string, null
value is passed
* to {@link Models#applyBindings(java.lang.Object, java.lang.String)} method.
* If the targetId
is not specified at all, no public
* applyBindings
method is generated at all (a change compared
* to previous versions of this API).
*
* @return an empty string (means apply globally), or ID of a (usually DOM)
* element to apply this model after calling its generated
* applyBindings()
method to
* @since 1.1
*/
String targetId() default "";
/** Controls whether builder-like setters shall be generated. Once this
* attribute is set, all {@link #properties()} will get a builder like
* setter (takes value of the property and returns this
* so invocations can be chained). When this attribute is specified,
* the non-default constructor isn't generated at all.
*
* Specifying builder="assign"
* and having {@link #properties() properties} name
and
* age
will generate method:
* public MyModel assignName(String name) { ... }
* public MyModel assignAge(int age) { ... }
*
* These methods can then be chained as
* MyModel m = new MyModel().assignName("name").assignAge(3);
*
* The builder
attribute can be set to empty string ""
-
* then it is possible that some property names clash with Java keywords.
* It's responsibility of the user to specify valid builder prefix,
* so the generated methods are compilable.
*
* @return the prefix to put before {@link Property property} names when
* generating their builder methods
* @since 1.3
*/
String builder() default "";
/** Controls keeping of per-instance private state. Sometimes
* the class generated by the {@link Model @Model annotation} needs to
* keep non-public, or non-JSON like state. One can achieve that by
* specifying instance=true
when using the annotation. Then
* the generated class gets a pointer to the instance of the annotated
* class (there needs to be default constructor) and all the {@link ModelOperation @ModelOperation},
* {@link Function @Function}, {@link OnPropertyChange @OnPropertyChange}
* and {@link OnReceive @OnReceive} methods may be non-static. The
* instance of the implementation class isn't accessible directly, just
* through calls to above defined (non-static) methods. Example:
*
* {@link Model @Model}(className="Data", instance=true, properties={
* {@link Property @Property}(name="message", type=String.class)
* })
* final class DataPrivate {
* private int count;
*
* {@link ModelOperation @ModelOperation} void increment(Data model) {
* count++;
* }
*
* {@link ModelOperation @ModelOperation} void hello(Data model) {
* model.setMessage("Hello " + count + " times!");
* }
* }
* Data data = new Data();
* data.increment();
* data.increment();
* data.increment();
* data.hello();
* assert data.getMessage().equals("Hello 3 times!");
*
*
* The methods annotated by {@link ComputedProperty} need to remain static, as
* they are supposed to be pure functions (e.g. depend only on their parameters)
* and shouldn't use any internal state.
*
* How do I initialize private values?
* The implementation class (the one annotated by {@link Model @Model} annotation)
* needs to have accessible default constructor. That constructor is used to
* create the instance. Obviously such constructor does not have
* any parameters, so no initialization is possible.
*
*
* Later one can, however, call any {@link ModelOperation @ModelOperation}
* method and pass in additional configuration parameters. In the above
* example it should be possible add
*
*
* {@link ModelOperation @ModelOperation} void init(Data model, int count) {
* this.count = count;
* }
*
* and then one can initialize the model using the init
as:
*
*
* Data data = new Data();
* data.init(2);
* data.increment();
* data.hello();
* assert data.getMessage().equals("Hello 3 times!");
*
* Why there has to be default constructor? Because instances of
* classes generated by {@link Model @Model annotation} may be constructed
* by the system as
* {@link Models#fromRaw(net.java.html.BrwsrCtx, java.lang.Class, java.lang.Object) wrappers around existing JavaScript objects}
* - then
* there is nobody to provide additional parameters at construction time.
*
* How do I read private values?
* The methods annotated by {@link ModelOperation} must return void
* (as they can run asynchronously) and as such they aren't suitable for
* returning values back to the caller. In case something like that is
* needed, one can use the approach of the hello
method - e.g.
* set value of some {@link Property property} that has a getter:
*
*
* data.hello();
* assert data.getMessage().equals("Hello 3 times!");
*
* Or one can use actor-like callbacks. Define callback interface and
* use it in a {@link ModelOperation @ModelOperation} method:
*
*
* public interface ReadCount {
* public void notifyCount(int count);
* }
* {@link ModelOperation @ModelOperation} void readCount(Data model, ReadCount callback) {
* callback.readCount(this.count);
* }
* Data data = new Data();
* data.init(2);
* data.increment();
* data.readCount(count -> System.out.println("count should be 3: " + count));
*
* The provided lambda-function callback may be invoked immediately
* or asynchronously as documentation for {@link ModelOperation}
* annotation describes.
*
*
* @return true
if the model class should keep pointer to
* instance of the implementation class
* @since 1.3
*/
boolean instance() default false;
}