@Retention(value=SOURCE) @Target(value=TYPE) public @interface Model
Model.className(). The generated class contains
getters and setters for properties defined via Model.properties() and
getters for other, derived properties defined by annotating methods
of this class by ComputedProperty. Each property
can be of primitive type, an enum type or (in order to create
nested JSON structure)
of another class generated by @Model annotation. Each property
can either represent a single value or be an array of its values.
The generated class's toString method
converts the state of the object into
JSON format. One can
use 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 @OnReceive annotation to read the model
asynchronously from a URL.
An example where one defines class Person with four
properties (firstName, lastName, array of addresses and
fullName) follows:
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@Model(className="Person", properties={@Property(name = "firstName", type=String.class),@Property(name = "lastName", type=String.class)@Property(name = "addresses", type=Address.class, array = true) }) static class PersonModel {@ComputedPropertystatic String fullName(String firstName, String lastName) { return firstName + " " + lastName; }@ComputedPropertystatic String mainAddress(List<Address>addresses) { for (Address a : addresses) { return a.getStreet() + " " + a.getTown(); } return "No address"; }@Model(className="Address", properties={@Property(name = "street", type=String.class),@Property(name = "town", type=String.class) }) static class AddressModel { } }
Model.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 Model.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>
model class (with appropriate
Knockout observable properties)
by calling 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:
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@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(Models.toRaw(p));
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.
model class instance
by copy and convert
the the whole object into plain
JSON. Just
print it as a string and parse it in JavaScript:
@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.
Model annotation or try
a little math test.| Modifier and Type | Required Element and Description |
|---|---|
String |
className
Name of the model class.
|
Property[] |
properties
List of properties in the model.
|
public abstract String className
public abstract Property[] properties
public abstract String targetId
applyBindings method
in the model class is going to be generated which later calls
Models.applyBindings(java.lang.Object, java.lang.String)
with appropriate targetId. If the targetId
is specified as empty string, null value is passed
to 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).applyBindings() method topublic abstract String builder
Model.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 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.property names when
generating their builder methodspublic abstract boolean instance
@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 @ModelOperation,
@Function, @OnPropertyChange
and @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:
@Model(className="Data", instance=true, properties={@Property(name="message", type=String.class) }) final class DataPrivate { private int count;@ModelOperationvoid increment(Data model) { count++; }@ModelOperationvoid 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 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 @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 @ModelOperation
method and pass in additional configuration parameters. In the above
example it should be possible add
@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 @Model annotation may be constructed
by the system as
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 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 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 @ModelOperation method:
public interface ReadCount {
public void notifyCount(int count);
}
@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 ModelOperation
annotation describes.
true if the model class should keep pointer to
instance of the implementation classCopyright © 2019 The Apache Software Foundation. All rights reserved.