Seam provides a convenient method of remotely accessing CDI beans from a web page, using AJAX (Asynchronous Javascript and XML). The framework for this functionality is provided with almost no up-front development effort - your beans only require simple annotating to become accessible via AJAX. This chapter describes the steps required to build an AJAX-enabled web page, then goes on to explain the features of the Seam Remoting framework in more detail.
To use remoting, the Seam Remoting servlet must first be configured in your web.xml
file:
<servlet>
<servlet-name>Remoting Servlet</servlet-name>
<servlet-class>org.jboss.seam.remoting.Remoting</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Remoting Servlet</servlet-name>
<url-pattern>/seam/resource/remoting/*</url-pattern>
</servlet-mapping>
The next step is to import the necessary Javascript into your web page. There are a minimum of two scripts that must be imported. The first one contains all the client-side framework code that enables remoting functionality:
<script type="text/javascript" src="seam/resource/remoting/resource/remote.js"></script>
For a production environment, you may wish to use a compressed version of remote.js
.
To do this, simply add the compress=true
parameter to the end of the url:
<script type="text/javascript" src="seam/resource/remoting/resource/remote.js?compress=true"></script>
The compressed version has its white space compacted and JavaScript comments removed. For development and debugging purposes it is recommended that you use the non-compacted version.
The second script that you need contains the stubs and type definitions for the beans you wish to call. It is
generated dynamically based on the method signatures of your beans, and includes type definitions for all of
the classes that can be used to call its remotable methods. The name of the script reflects the
name of your bean. For example, if you have a named bean annotated with @Named
, then your script
tag should look like this (for a bean class called CustomerAction
):
<script type="text/javascript"
src="seam/resource/remoting/interface.js?customerAction"></script>
Otherwise, you can simply specify the fully qualified class name of the bean:
<script type="text/javascript"
src="seam/resource/remoting/interface.js?com.acme.myapp.CustomerAction"></script>
If you wish to access more than one bean from the same page, then include them all as parameters of your script tag:
<script type="text/javascript"
src="seam/resource/remoting/interface.js?customerAction&accountAction"></script>
Client-side interaction with your beans is all performed via the Seam
Javascript
object. This object is defined in remote.js
, and you'll be using it to make asynchronous calls
against your bean. It contains methods for creating client-side bean objects and also methods for executing remote
requests. The easiest way to become familiar with this object is to start with a simple example.
Let's step through a simple example to see how the Seam
object works. First of all,
let's create a new bean called helloAction
:
@Named
public class HelloAction implements HelloLocal {
@WebRemote public String sayHello(String name) {
return "Hello, " + name;
}
}
Take note of the @WebRemote
annotation on the sayHello()
method in the
above listing. This annotation makes the method accessible via the Remoting API. Besides this annotation, there's
nothing else required on your bean to enable it for remoting.
Now for our web page - create a new JSF page and import the helloAction
bean:
<script type="text/javascript"
src="seam/resource/remoting/interface.js?helloAction
To make this a fully interactive user experience, let's add a button to our page:
<button onclick="javascript:sayHello()">Say Hello</button>
We'll also need to add some more script to make our button actually do something when it's clicked:
<script type="text/javascript">
//<![CDATA[
function sayHello() {
var name = prompt("What is your name?");
Seam.createBean("helloAction").sayHello(name, sayHelloCallback);
}
function sayHelloCallback(result) {
alert(result);
}
// ]]>
</script>
We're done! Deploy your application and open the page in a web browser. Click the button, and enter a name when
prompted. A message box will display the hello message confirming that the call was successful. If you want to
save some time, you'll find the full source code for this Hello World example in the
/examples/helloworld
directory.
So what does the code of our script actually do? Let's break it down into smaller pieces. To start with, you can see from the Javascript code listing that we have implemented two methods - the first method is responsible for prompting the user for their name and then making a remote request. Take a look at the following line:
Seam.createBean("helloAction").sayHello(name, sayHelloCallback);
The first section of this line, Seam.createBean("helloAction")
returns a
proxy, or "stub" for our helloAction
bean. We can invoke the methods of our bean
against this stub, which is exactly what happens with the remainder of the line:
sayHello(name, sayHelloCallback);
.
What this line of code in its completeness does, is invoke the sayHello
method of our
bean, passing in name
as a parameter. The second parameter,
sayHelloCallback
isn't a parameter of our bean's sayHello
method,
instead it tells the Seam Remoting framework that once it receives the response to our request, it should pass
it to the sayHelloCallback
Javascript method. This callback parameter is entirely optional,
so feel free to leave it out if you're calling a method with a void
return type or if you
don't care about the result.
The sayHelloCallback
method, once receiving the response to our remote request then pops
up an alert message displaying the result of our method call.
The Seam.createBean
JavaScript method is used to create client-side instances of both
action and "state" beans. For action beans (which are those that contain one or more methods annotated with
@WebRemote
), the stub object provides all of the remotable methods exposed by the bean.
For "state" beans (i.e. beans that simply carry state, for example Entity beans) the stub object provides all
the same accessible properties as its server-side equivalent. Each property also has a corresponding
getter/setter method so you can work with the object in JavaScript in much the same way as you would in Java.
The Seam Remoting Context contains additional information which is sent and received as part of a remoting request/response cycle. It currently contains the conversation ID and Call ID, and may be expanded to include other properties in the future.
If you intend on using remote calls within the scope of a conversation then you need to be able to read or
set the conversation ID in the Seam Remoting Context. To read the conversation ID after making a remote request
call Seam.context.getConversationId()
. To set the conversation ID before making a
request, call Seam.context.setConversationId()
.
If the conversation ID hasn't been explicitly set with
Seam.context.setConversationId()
, then it will be automatically assigned the
first valid conversation ID that is returned by any remoting call. If you are working with multiple conversations
within your page, then you may need to explicitly set the conversation ID before each call. If you are working
with just a single conversation, then you don't need to do anything special.
In some circumstances it may be required to make a remote call within the scope of the current view's conversation. To do this, you must explicitly set the conversation ID to that of the view before making the remote call. This small snippet of JavaScript will set the conversation ID that is used for remoting calls to the current view's conversation ID:
Seam.context.setConversationId( #{conversation.id} );
This section describes the support for basic data types. On the server side these values as a rule are compatible with either their primitive type or their corresponding wrapper class.
There is support for all number types supported by Java. On the client side, number values are always
serialized as their String representation and then on the server side they are converted to the correct
destination type. Conversion into either a primitive or wrapper type is supported for Byte
,
Double
, Float
, Integer
, Long
and
Short
types.
In general these will be either entity beans or JavaBean classes, or some other non-bean class. Use
Seam.createBean()
to create a new instance of the object.
Date values are serialized into a String representation that is accurate to the millisecond. On the client
side, use a JavaScript Date
object to work with date values. On the server side, use any
java.util.Date
(or descendent, such as java.sql.Date
or
java.sql.Timestamp
class.
On the client side, enums are treated the same as String
s. When setting the value for an enum parameter,
simply use the String
representation of the enum. Take the following bean as an example:
@Named
public class paintAction {
public enum Color {red, green, blue, yellow, orange, purple};
public void paint(Color color) {
// code
}
}
To call the paint()
method with the color red
, pass the parameter
value as a String
literal:
Seam.createBean("paintAction").paint("red");
The inverse is also true - that is, if a bean method returns an enum parameter (or contains an enum
field anywhere in the returned object graph) then on the client-side it will be converted to a String
.
Bags cover all collection types including arrays, collections, lists, sets, (but excluding Maps - see the next section for those), and are implemented client-side as a JavaScript array. When calling a bean method that accepts one of these types as a parameter, your parameter should be a JavaScript array. If a bean method returns one of these types, then the return value will also be a JavaScript array. The remoting framework is clever enough on the server side to convert the bag to an appropriate type (including sophisticated support for generics) for the bean method call.
As there is no native support for Maps within JavaScript, a simple Map implementation is provided with
the Seam Remoting framework. To create a Map which can be used as a parameter to a remote call, create a new
Seam.Map
object:
var map = new Seam.Map();
This JavaScript implementation provides basic methods for working with Maps: size()
,
isEmpty()
, keySet()
, values()
,
get(key)
, put(key, value)
, remove(key)
and
contains(key)
. Each of these methods are equivalent to their Java counterpart. Where the
method returns a collection, such as keySet()
and values()
, a JavaScript
Array object will be returned that contains the key or value objects (respectively).
To aid in tracking down bugs, it is possible to enable a debug mode which will display the contents of all
the packets send back and forth between the client and server in a popup window. To enable debug mode, set the
Seam.debug
property to true
in Javascript:
Seam.debug = true;
If you want to write your own messages to the debug log, call
Seam.log(message)
.
When invoking a remote bean method, it is possible to specify an exception handler which will process the response in the event of an exception during bean invocation. To specify an exception handler function, include a reference to it after the callback parameter in your JavaScript:
var callback = function(result) { alert(result); }; var exceptionHandler = function(ex) { alert("An exception occurred: " + ex.getMessage()); }; Seam.createBean("helloAction").sayHello(name, callback, exceptionHandler);
If you do not have a callback handler defined, you must specify null
in its place:
var exceptionHandler = function(ex) { alert("An exception occurred: " + ex.getMessage()); }; Seam.createBean("helloAction").sayHello(name, null, exceptionHandler);
The exception object that is passed to the exception handler exposes one method, getMessage()
that returns the exception message which is produced by the exception thrown by the @WebRemote
method.
The default loading message that appears in the top right corner of the screen can be modified, its rendering customised or even turned off completely.
To change the message from the default "Please Wait..." to something different, set the value of
Seam.loadingMessage
:
Seam.loadingMessage = "Loading...";
To completely suppress the display of the loading message, override the implementation of
displayLoadingMessage()
and hideLoadingMessage()
with functions that
instead do nothing:
// don't display the loading indicator
Seam.displayLoadingMessage = function() {};
Seam.hideLoadingMessage = function() {};
It is also possible to override the loading indicator to display an animated icon, or anything else that
you want. To do this override the displayLoadingMessage()
and
hideLoadingMessage()
messages with your own implementation:
Seam.displayLoadingMessage = function() {
// Write code here to display the indicator
};
Seam.hideLoadingMessage = function() {
// Write code here to hide the indicator
};
When a remote method is executed, the result is serialized into an XML response that is returned to the client. This response is then unmarshaled by the client into a JavaScript object. For complex types (i.e. Javabeans) that include references to other objects, all of these referenced objects are also serialized as part of the response. These objects may reference other objects, which may reference other objects, and so forth. If left unchecked, this object "graph" could potentially be enormous, depending on what relationships exist between your objects. And as a side issue (besides the potential verbosity of the response), you might also wish to prevent sensitive information from being exposed to the client.
Seam Remoting provides a simple means to "constrain" the object graph, by specifying the
exclude
field of the remote method's @WebRemote
annotation. This field
accepts a String array containing one or more paths specified using dot notation. When invoking a remote method,
the objects in the result's object graph that match these paths are excluded from the serialized result packet.
For all our examples, we'll use the following Widget
class:
public class Widget
{
private String value;
private String secret;
private Widget child;
private Map<String,Widget> widgetMap;
private List<Widget> widgetList;
// getters and setters for all fields
}
If your remote method returns an instance of Widget
, but you don't want to expose the
secret
field because it contains sensitive information, you would constrain it like this:
@WebRemote(exclude = {"secret"})
public Widget getWidget();
The value "secret" refers to the secret
field of the returned object. Now, suppose that
we don't care about exposing this particular field to the client. Instead, notice that the
Widget
value that is returned has a field child
that is also a
Widget
. What if we want to hide the child
's secret
value instead? We can do this by using dot notation to specify this field's path within the result's object
graph:
@WebRemote(exclude = {"child.secret"})
public Widget getWidget();
The other place that objects can exist within an object graph are within a Map
or some
kind of collection (List
, Set
, Array
, etc). Collections
are easy, and are treated like any other field. For example, if our Widget
contained a list
of other Widget
s in its widgetList
field, to constrain the
secret
field of the Widget
s in this list the annotation would look like
this:
@WebRemote(exclude = {"widgetList.secret"})
public Widget getWidget();
To constrain a Map
's key or value, the notation is slightly different. Appending
[key]
after the Map
's field name will constrain the
Map
's key object values, while [value]
will constrain the value object
values. The following example demonstrates how the values of the widgetMap
field have their
secret
field constrained:
@WebRemote(exclude = {"widgetMap[value].secret"})
public Widget getWidget();
There is one last notation that can be used to constrain the fields of a type of object no matter where in the result's object graph it appears. This notation uses either the name of the bean (if the object is a named bean) or the fully qualified class name (only if the object is not a named bean) and is expressed using square brackets:
@WebRemote(exclude = {"[widget].secret"})
public Widget getWidget();
The Model API builds on top of Seam Remoting's object serialization features to provide a component-based approach to working with a server-side object model, as opposed to the RPC-based approach provided by the standard Remoting API. This allows a client-side representation of a server-side object graph to be modified ad hoc by the client, after which the changes made to the objects in the graph can be applied to the corresponding server-side objects. When applying the changes the client determines exactly which objects have been modified by recursively walking the client-side object tree and generating a delta by comparing the original property values of the objects with their new property values.
This approach, when used in conjunction with the extended persistence context provided by Seam elegantly solves a number of problems faced by AJAX developers when working remotely with persistent objects. A persistent, managed object graph can be loaded at the start of a new conversation, and then across multiple requests (and within the same transaction) the client can fetch the objects, make changes to them and apply those changes to the same managed objects after which the long-running transaction can be committed when the conversation ends.
One other useful feature of the Model API is its ability to expand a model.
For example, if you are working with entities with lazy-loaded associations it is usually not a good idea
to blindly fetch the associated objects (which may in turn themselves contain associations
to other entities, ad nauseum), as you may inadvertently end up fetching the bulk of your database.
Seam Remoting already knows how to deal with lazy-loaded associations by automatically excluding
them when marshalling instances of entity beans, and assigning them a client-side value of
undefined
(which is a special JavaScript value, distinct from null
).
The Model API goes one step further by giving the client the option of manipulating the associated objects
also. By providing an expand operation, it allows for the initialization of a
previously-uninitialized object property (such as a lazy-loaded collection), by dynamically "grafting"
the initialized value onto the object graph. By expanding the model in this way,
we have at our disposal a powerful tool for building dynamic client interfaces.
For the methods of the Model API that accept action parameters, an instance of
Seam.Action
should be used. The constructor for
Seam.Action
takes no parameters:
var action = new Seam.Action();
The following table lists the methods used to define the action. Each of the following methods
return a reference to the Seam.Action
object, so methods can be chained.
Table 2.1. Seam.Action method reference
Method |
Description |
---|---|
|
Sets the class name of the bean to be invoked.
|
|
Sets the qualifiers for the bean to be invoked.
|
|
Sets the name of the bean method.
|
|
Adds a parameter value for the action method. This method should be called once for each parameter value to be added, in the correct parameter order.
|
The following table describes the methods provided by the Seam.Model
object. To work with
the Model API in JavaScript you must first create a new Model object:
var model = new Seam.Model();
Table 2.2. Seam.Model method reference
Method |
Description |
---|---|
|
Adds a bean value to the model. When the model is fetched, the value of the specified bean
will be read and placed into the model, where it may be accessed by using the
Can only be used before the model is fetched.
|
|
Adds a bean property value to the model. When the model is fetched, the value of the specified
property on the specified bean will be read and placed into the model, where it may be accessed
by using the Can only be used before the model is fetched. Example: addBeanProperty("account", "AccountAction", "account", "@Qualifier1", "@Qualifier2");
|
|
Fetches the model - this operation causes an asynchronous request to be sent to the server.
The request contains a list of the beans and bean properties (set by calling the
A model should only be fetched once.
|
|
This method returns the value of the object with the specified alias.
|
|
Expands the model by initializing a property value that was previously uninitialized. This operation causes an asynchronous request to be sent to the server, where the uninitialized property value (such as a lazy-loaded collection within an entity bean association) is initialized and the resulting value is returned to the client. Once the response is received, the callback method (if specified) will be invoked, passing in a reference to the model as a parameter.
|
|
Applies the changes made to the objects contained in the model. This method causes an asynchronous request to be sent to the server containing a delta consisting of a list of the changes made to the client-side objects.
|
To fetch a model, one or more values must first be specified using addBean()
or
addBeanProperty()
before invoking the fetch()
operation.
Let's work through an example - here we have an entity bean called Customer
:
@Entity Customer implements Serializable {
private Integer customerId;
private String firstName;
private String lastName;
@Id @GeneratedValue public Integer getCustomerId() { return customerId; }
public void setCustomerId(Integer customerId) { this.customerId = customerId; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
We also have a bean called CustomerAction
, which is responsible for creating and editing
Customer
instances. Since we're only interested in editing a customer right now, the
following code only shows the editCustomer()
method:
@ConversationScoped @Named
public class CustomerAction {
@Inject Conversation conversation;
@PersistenceContext EntityManager entityManager;
public Customer customer;
public void editCustomer(Integer customerId) {
conversation.begin();
customer = entityManager.find(Customer.class, customerId);
}
public void saveCustomer() {
entityManager.merge(customer);
conversation.end();
}
}
In the client section of this example, we wish to make changes to an existing Customer
instance, so we need to use the editCustomer()
method of CustomerAction
to first load the customer entity, after which we can access it via the public customer
field. Our model object must therefore be configured to fetch the CustomerAction.customer
property, and to invoke the editCustomer()
method when the model is fetched. We start
by using the addBeanProperty()
method to add a bean property to the model:
var model = new Seam.Model();
model.addBeanProperty("customer", "CustomerAction", "customer");
The first parameter of addBeanProperty()
is the alias (in this case
customer
), which is used to access the value via the getValue()
method.
The addBeanProperty()
and addBean()
methods can be called multiple times
to bind multiple values to the model. An important thing to note is that the values may come from multiple
server-side beans, they aren't all required to come from the same bean.
We also specify the action that we wish to invoke (i.e. the editCustomer()
method).
In this example we know the value of the customerId
that we wish to edit, so we can
specify this value as an action method parameter:
var action = new Seam.Action()
.setBeanType("CustomerAction")
.setMethod("editCustomer")
.addParam(123);
Once we've specified the bean properties we wish to fetch and the action to invoke, we can then fetch the
model. We pass in a reference to the action object as the first parameter of the fetch()
method. Also, since this is an asynchronous request we need to provide a callback method to deal with the
response. The callback method is passed a reference to the model object as a parameter.
var callback = function(model) { alert("Fetched customer: " model.getValue("customer").firstName +
" " + model.getValue("customer").lastName); };
model.fetch(action, callback);
When the server receives a model fetch request, it first invokes the action (if one is specified) before reading the requested property values and returning them to the client.
Alternatively, if you don't wish to fetch a bean property but rather a bean itself
(such as a value created by a producer method) then the addBean()
method is used instead.
Let's say we have a producer method that returns a qualified UserSettings
value:
@Produces @ConversationScoped @Settings UserSettings getUserSettings() {
/* snip code */
}
We would add this value to our model with the following code:
model.addBean("settings", "UserSettings", "@Settings");
The first parameter is the local alias for the value, the second parameter is the fully qualified class of the bean, and the third (and subsequent) parameter/s are optional bean qualifiers.
Once a model has been fetched its values may be read using the getValue()
method.
Continuing on with the previous example, we would retrieve the Customer
object via
it's local alias (customer
) like this:
var customer = model.getValue("customer");
We are then free to read or modify the properties of the value (or any of the other values within its object graph).
alert("Customer name is: " + customer.firstName + " " + customer.lastName);
customer.setLastName("Jones"); // was Smith, but Peggy got married on the weekend
We can use the Model API's ability to expand a model to load uninitialized branches of the objects in
the model's object graph. To understand how this works exactly, let's flesh out our example a little
more by adding an Address
entity class, and creating a one-to-many relationship
between Customer
and Address
.
@Entity Address implements Serializable {
private Integer addressId;
private Customer customer;
private String unitNumber;
private String streetNumber;
private String streetName;
private String suburb;
private String zip;
private String state;
private String country;
@Id @GeneratedValue public Integer getAddressId() { return addressId; }
public void setAddressId(Integer addressId) { this.addressId = addressId; }
@ManyToOne public Customer getCustomer() { return customer; }
public void setCustomer(Customer customer) { this.customer = customer; }
/* Snipped other getter/setter methods */
}
Here's the new field and methods that we also need to add to the Customer
class:
private Collection<Address> addresses;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "customer", cascade = CascadeType.ALL)
public Collection<Address> getAddresses() { return addresses; }
public void setAddresses(Collection<Address> addresses) { this.addresses = addresses; }
As we can see, the @OneToMany
annotation on the getAddresses()
method specifies a fetch
attribute of LAZY
, meaning that by
default the customer's addresses won't be loaded automatically when the customer is. When reading the
uninitialized addresses
property value from a newly-fetched
Customer
object in JavaScript, a value of undefined
will be returned.
getValue("customer").addresses == undefined; // returns true
We can expand the model by making a special request to initialize this uninitialized
property value. The expand()
operation takes three parameters - the value containing
the property to be initialized, the name of the property and an optional callback method. The following
example shows us how the customer's addresses
property can be initialized:
model.expand(model.getValue("customer"), "addresses");
The expand()
operation makes an asynchronous request to the server, where the
property value is initialized and the value returned to the client. When the client receives the
response, it reads the initialized value and appends it to the model.
// The addresses property now contains an array of address objects
alert(model.getValue("customer").addresses.length + " addresses loaded");
Once you have finished making changes to the values in the model, you can apply them with the
applyUpdates()
method. This method scans all of the objects in the model, compares
them with their original values and generates a delta which may contain one or more changesets to
send to the server. A changeset is simply a list of property value changes for a single object.
Like the fetch()
command you can also specify an action to invoke when applying updates,
although the action is invoked after the model updates have been applied. In a
typical situation the invoked action would do things like commit a database transaction, end the current
conversation, etc.
Since the applyUpdates()
method sends an asynchronous request like the
fetch()
and expand()
methods, we also need to specify a callback
function if we wish to do something when the operation completes.
var action = new Seam.Action();
.setBeanType("CustomerAction")
.setMethod("saveCustomer");
var callback = function() { alert("Customer saved."); };
model.applyUpdates(action, callback);
The applyUpdates()
method performs a refresh of the model, retrieving the latest
state of the objects contained in the model after all updates have been applied and the action method
(if specified) invoked.