SeamFramework.orgCommunity Documentation

Seam Remoting


1. Seam Remoting - Basic Features
1.1. Configuration
1.1.1. Dynamic type loading
1.2. The "Seam" object
1.2.1. A Hello World example
1.2.2. Seam.createBean
1.3. The Context
1.3.1. Setting and reading the Conversation ID
1.3.2. Remote calls within the current conversation scope
1.4. Working with Data types
1.4.1. Primitives / Basic Types
1.4.2. JavaBeans
1.4.3. Dates and Times
1.4.4. Enums
1.4.5. Collections
1.5. Debugging
1.6. Handling Exceptions
1.7. The Loading Message
1.7.1. Changing the message
1.7.2. Hiding the loading message
1.7.3. A Custom Loading Indicator
1.8. Controlling what data is returned
1.8.1. Constraining normal fields
1.8.2. Constraining Maps and Collections
1.8.3. Constraining objects of a specific type
1.8.4. Combining Constraints
2. Seam Remoting - Bean Validation
2.1. Validating a single object
2.2. Validating a single property
2.3. Validating multiple objects and/or properties
2.4. Validation groups
2.5. Handling validation failures
3. Seam Remoting - Model API
3.1. Introduction
3.2. Model Operations
3.3. Fetching a model
3.3.1. Fetching a bean value
3.4. Modifying model values
3.5. Expanding a model
3.6. Applying Changes

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>

By default, the client-side JavaScript is served in compressed form, with white space compacted and JavaScript comments removed. For a development environment, you may wish to use the uncompressed version of remote.js for debugging and testing purposes. To do this, simply add the compress=false parameter to the end of the url:


<script type="text/javascript" src="seam/resource/remoting/resource/remote.js?compress=false"></script>

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 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.

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 two methods, getExceptionClass() which returns the name of the exception class that was thrown, and getMessage(), which returns the exception message which is produced by the exception thrown by the @WebRemote method.

It is also possible to register a global exception handler, which will be invoked if there is no exception handler defined for an individual invocation. By default, the global exception handler will display an alert message notifying the user that there was an exception - here's what the default exception handler looks like:

Seam.defaultExceptionHandler = function(exception) {
  alert("An exception has occurred while executing a remote request: " + exception.getExceptionClass() + ":" + exception.getMessage());
};

If you would like to provide your own global exception handler, then simply override the value of Seam.exceptionHandler with your own custom exception handler, as in the following example:

  function customExceptionHandler(exception) {
    alert("Uh oh, something bad has happened! [" + exception.getExceptionClass() + ":" + exception.getMessage() + "]");
  }    
  
  Seam.exceptionHandler = customExceptionHandler;
    

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
}

Seam Remoting provides integrated support for JSR-303 Bean Validation, which defines a standard approach for validating Java Beans no matter where they are used; web tier or persistence tier, server or client. Bean validation for remoting delivers JSR-303's vision by making all of the validation constraints declared by the server-side beans available on the client side, and allows developers to perform client-side bean validation in an easy to use, consistent fashion.

Client-side validation by its very nature is an asynchronous operation, as it is possible that the client may encounter a custom validation constraint for which it has no knowledge of the corresponding validation logic. Under these circumstances, the client will make a request to the server for the validation to be performed server-side, after which it receives the result will forward it to the client-side callback method. All built-in validation types defined by the JSR-303 specification are executed client-side without requiring a round-trip to the server. It is also possible to provide the client-side validation API with custom JavaScript to allow client-side execution of custom validations.

It is also possible to perform multiple validations for beans and bean properties in one go. This might be useful for example to perform validation of forms that present data from more than one bean. The Seam.validate() method takes the following parameters:

      Seam.validate(validations, callback, groups);
    

The validations parameter should contain a list of the validations to perform. It may either be an associative array (for a single validation), or an array of associative arrays (for multiple validations) which define the validations that should be performed. We'll look at this parameter more closely in just a moment.

The callback parameter should contain a reference to the callback function to invoke once validation is complete. The optional groups parameter should contain the group name/s for which to perform validation.

The groups parameter allows one or more validation groups (specified by providing a String or array of String values) to be validated. The validation groups specified here will be applied to all bean values contained in the validations parameter.

The simplest example, in which we wish to validate a single object would look like this:

  Seam.validate({bean:customer}, callback);

In the above example, validation will be performed for the customer object, after which the function named validationCallback will be invoked.

Validate multiple beans is done by passing in an array of validations:

  Seam.validate([{bean:customer}, {bean:order}], callback);

Single properties can be validated by specifying a property name:

  Seam.validate({bean:customer, property: "firstName"}, callback);

To prevent the entire object graph from being validated, the traverse property may be set to false:

  Seam.validate({bean:customer, traverse: false}, callback);

Validation groups may also be set for each individual validation, by setting the groups property to a String or array of Strings value:

  Seam.validate({bean:customer, groups: "default"}, callback);

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 the client can fetch the objects, make incremental changes to them and apply those changes to the same managed objects after which the transaction can be committed, thereby persisting the changes made.

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.


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 3.2. Seam.Model method reference

Method

Description

addBean(alias, bean, qualifiers)

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 getValue() method with the specified alias.

Can only be used before the model is fetched.

  • alias - the local alias for the bean value.
  • bean - the name of the bean, either specified by the @Named annotation or the fully qualified class name.
  • qualifiers (optional) - a list of bean qualifiers.

addBeanProperty(alias, bean, property, qualifiers)

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 getValue() method with the specified alias.

Can only be used before the model is fetched.

Example:


  addBeanProperty("account", "AccountAction", "account", "@Qualifier1", "@Qualifier2");
  • alias - the local alias for the bean value.
  • bean - the name of the bean, either specified by the @Named annotation or the fully qualified class name.
  • property - the name of the bean property.
  • qualifiers (optional) - a list of bean qualifiers. This parameter (and any after it) are treated as bean qualifiers.

fetch(action, callback)

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 addBean() and addBeanProperty() methods) for which values will be returned. Once the response is received, the callback method (if specified) will be invoked, passing in a reference to the model as a parameter.

A model should only be fetched once.

  • action (optional) - a Seam.Action instance representing the bean action to invoke before the model values are read and stored in the model.
  • callback (optional) - a reference to a JavaScript function that will be invoked after the model has been fetched. A reference to the model instance is passed to the callback method as a parameter.

getValue(alias)

This method returns the value of the object with the specified alias.

  • alias - the alias of the value to return.

expand(value, property, callback)

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.

  • value - a reference to the value containing the uninitialized property to fetch. This can be any value within the model, and does not need to be a "root" value (i.e. it doesn't need to be a value specified by addBean() or addBeanProperty(), it can exist anywhere within the object graph.
  • property - the name of the uninitialized property to be initialized.
  • callback (optional) - a reference to a JavaScript function that will be invoked after the model has been expanded. A reference to the model instance is passed to the callback method as a parameter.

applyUpdates(action, callback)

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.

  • action (optional) - a Seam.Action instance representing a bean method to be invoked after the client-side model changes have been applied to their corresponding server-side objects.
  • callback (optional) - a reference to a JavaScript function that will be invoked after the updates have been applied. A reference to the model instance is passed to the callback method as a parameter.

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.

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");