ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


JBoss Seam

by Thomas Heute
03/15/2006

JavaServer Faces (JSF) and Enterprise JavaBeans (EJB) 3.0 are great technologies for building web applications. There is a significant deal of synergy between the two frameworks: stateless and stateful EJBs are excellent event-handling backing beans for JSF. EJB entity beans provide automatic and transparent object/relational database mapping; hence, they are a fine solution for implementing an object-oriented domain model. Visible JSF components can bind their state to entity beans. You may even want to add some business process management (BPM) into your application.

However, while the above described synergy between EJB 3.0 and JSF is appealing, it is not yet realized within the Java Enterprise Edition (JEE) 5.0 framework. In plain Java EE, there is still a significant amount of glue code necessary to use both JSF and EJB 3.0 when building a web application--even more, if you add BPM to the recipe for a great application. In particular, many problems inherent to the web application, such as the dreaded "Back button problem" or multi-window operations, require time spent on the infrastructure coding rather than on the actual business problem.

The JBoss Seam framework is designed to take care of the plumbing between existing frameworks including EJB 3.0, JSF, and BPM. The Seam stateful component model makes it a breeze to develop sophisticated stateful web applications. In this article, we will show how to use Seam to create an e-commerce application--the JBoss Seam DVD Store application.

Bringing the EJB and Web Together

Traditionally, developers apply many design patterns in a sophisticated web application, creating layers of code, which is often a requirement of the frameworks, not of the application design. Seam collapses these artificial layers and brings EJBs closer to the web layer. It allows you to access EJBs as JSF backing beans. Hence, a developer is free to choose what layers are needed according to the application's architecture requirements, instead of being forced into decisions by the framework.

Seam manages the lifecycles of the application components automatically. The developer interacts with components by retrieving and storing them from and into the Seam context. This design is also known as "Inversion of Control," and it frees the developers from manually managing the relationships between the components.

To illustrate our point, let's look an example from a hypothetical DVD store. This example shows how to retrieve orders already made by the logged-on user on a single web page. Let's first have a look at our stateful session bean, which executes the business logic and performs all database-related operations. This session bean will be in charge of finding all of the orders of a logged-in user. Please note that we chose to show a simple way to perform this task. You can certainly keep your habits of having value objects in between, and remember that Seam components can be any object, not only EJBs.

package com.jboss.dvd.seam;
import [...]

@Stateful
@Name("showorders")
@Interceptors(SeamInterceptor.class)
public class ShowOrdersAction
    implements ShowOrders,
               Serializable
{
    @In(value="currentUser",required=false)
    Customer customer;

    @PersistenceContext(unitName="dvd")
    EntityManager em;

    @DataModel
    List <Order> orders;    

    @Factory("orders")
    public String findOrders() {
        orders = em.createQuery(
   "from Order o where o.customer = :customer")
            .setParameter("customer", customer)
            .getResultList();
        return "showorders";
    }

    @Destroy 
    @Remove
    public void destroy() {}
}

This bean is a standard EJB3 stateful bean. The @Stateful annotation is used to declare that bean as a stateful session bean and the @Remove annotation is used to ask the server to remove that bean once the destroy method is called.

The session bean class also has some annotations beyond the EJB 3.0 specification. These are Seam annotations. The @Name annotation gives a reference name to the session bean so that it can be called from any JSF page or other Seam components. We also specify the SeamInterceptor as the only interceptor for this session bean. This is required for Seam to work out the "plumbing" for the bean.

JBoss: A Developer's Notebook

Related Reading

JBoss: A Developer's Notebook
By Norman Richards, Sam Griffith

Since the JSF page that uses this session bean needs to retrieve the current orders for a logged-in user, we need to first figure out who the currently logged-in user is. The current user has been stored in the session context just after he successfully logged on, from the session bean, so we just want to inject that entity bean into the "customer" object. @In(value="currentUser", required=false) will then look for a reference to "currentUser" in all of the stateful contexts and will not attempt to create one if it can't find one. Once found, it will be injected in the "customer" object. This operation will happen before any session bean method is called.

After getting the currently logged-in user, we still need to populate the "orders" list so that the JSF page can display. The @Factory("orders") annotation will do the job of populating that object and the annotated method will be called before the JSF page can access the orders.

Now, we have the business component (session bean) to retrieve the orders. Let's look at the JSF web page and how Seam ties the session bean to the JSF page. Basically, in the showorders.jsf page, we just need to reference the session bean and its properties using their Seam names.

<h:dataTable value="#{orders}" var="item">
   <h:column>
      <f:facet name="header">Order Id</f:facet>
      #{item.orderId}
   </h:column>                        
   <h:column>
      <f:facet name="header">Date</f:facet>
      <h:outputText value="#{item.orderDate}">
          <f:convertDateTime type="both" 
                             dateStyle="full"/> 
      </h:outputText>
   </h:column>          
   <h:column>
      <f:facet name="header">Status</f:facet>
      #{item.status}
  </h:column>
</h:dataTable>

A Contextual Programming Model

Besides EJB 3.0 and JSF integration, Seam is an advanced contextual management framework of its own. It provides crucial features that make it easy to develop stateful web applications. A context is something that is available in many Java programming models. A web application typically has to deal with the Request context, which has the scope of a particular event. The currently logged-in user of the application is held in the Session context. Seam recognizes the importance of these contexts, and adds a few of its own to complete the picture:

Seam creates all of these contexts and manages their scope automatically. A Seam developer can declare a default context for each component; for example, a User component could have Session scope, representing the currently logged-in user. The power of Seam lies in the wiring of components. A component simply has to declare what dependencies have to be injected into it, before it is executed. A component can also declare what objects should be "taken out" of it, once execution completes. This "outjection" feature, together with the automatic management of contexts and component state, is at the core of programming with Seam and allows very compact and re-usable code.

In the following example, we will look at how we can build a search page to search for DVDs, as illustrated in Figure 1.

Figure 1
Figure 1. The DVD search result page

We want to ensure that back buttons and multiple tabs or windows won't break our application and have the expected behavior. To do so, we will introduce a conversation that will start whenever the search starts and ends once the user purchases his order. Let's start with a session bean that will handle the search operations and navigation among the results:

package com.jboss.dvd.seam;
import [...]

@Stateful
@Name("search")
@Conversational(ifNotBegunOutcome="main")
@Scope(ScopeType.CONVERSATION)
@Interceptors(SeamInterceptor.class)
public class SearchAction
    implements Search,
               Serializable
{
    @In(create=true)
    ShoppingCart cart;

    @PersistenceContext(unitName="dvd")
    EntityManager em;

    private int     pageSize    = 20;
    private int     currentPage = 0; 
    private boolean hasMore     = false;

    private Category category = null;
    private String   title    = null;
    private String   actor    = null;

    @Out(scope=CONVERSATION,required=false)
    List <Product> searchResults;

    @Out(scope=CONVERSATION,required=false)
    Map <Product, Boolean> searchSelections;

    // Getters and setters for 
    // category, title and actor

    @Begin(join=true, 
           processDefinition="shopping")
    public String doSearch() {
        currentPage=0;
        updateResults();

        return "browse";
    }

    public String nextPage() {
        if (!isLastPage()) {
            currentPage++;
            updateResults();
        }
        return null;
    }

    public String prevPage() {
        if (!isFirstPage()) {
            currentPage--;
            updateResults();
        }
        return null;
    }

    public boolean isLastPage() {
        return (searchResults != null) && 
               !hasMore;
    }
    public boolean isFirstPage() {
        return (searchResults != null) && 
               (currentPage == 0);
    }

    private void updateResults() {
        [...]
        searchResults = [...]
        searchSelections = [...]
    }


    private Query searchQuery(String title, 
           String actor, Category category) {
        title = (title == null) ? "%" : "%" + 
                   title.toLowerCase() + "%";
        actor = (actor == null) ? "%" : "%" + 
                   actor.toLowerCase() + "%";

        if (category == null) {
            return em.createQuery(
    "from Product p where lower(p.title) like " + 
    ":title and lower(p.actor) LIKE :actor")
                .setParameter("title", title)
                .setParameter("actor", actor);
        } 
        else { 
            return em.createQuery(
    "from Product p where lower(p.title) like " + 
    ":title and lower(p.actor) like :actor " + 
    "and p.category = :category")
                .setParameter("title", title)
                .setParameter("actor", actor)
                .setParameter("category", category);
        }
    }

    public String addToCart() {
        for (Product item: searchResults) {
            Boolean selected = 
              searchSelections.get(item);
            if ( selected!=null && selected ) {
                searchSelections.put(item, false);
                cart.addProduct(item, 1);
            }
        }
        return "browse";
    }

    @Destroy 
    @Remove
    public void destroy() {}   
}

This stateful session bean is stored in the conversation context; it will live throughout the conversation and die whenever the conversation ends. The shopping cart is another session-bean component managed by Seam and is injected into the SearchAction session bean, so that we don't lose its content across multiple searches. The searchResults and searchSelections objects will live in the current conversation; the memory they occupy will be freed once the conversation will be over.

The most important feature that is shown here is the beginning of the conversation itself; the @Begin(join=true, processDefinition="shopping") on the doSearch() method will start a new conversation using the pageflow definition called shopping. This jBPM pageflow definition will define how the pages are related and define all of the transitions for this particular context. Here is our simple example:

<pageflow-definition [...]
               name="shopping">

   <start-state name="start">
      <transition to="browse"/>
   </start-state>
   
   <page name="browse" view-id="/browse.xhtml" 
                          redirect="true">
      <transition name="browse" to="browse"/>
      <transition name="checkout" to="checkout"/>
   </page>
   
   <page name="checkout" view-id="/checkout.xhtml" 
                            redirect="true">
      <transition name="checkout" to="checkout"/>
      <transition name="complete" to="complete"/>
   </page>
   
   <page name="complete" view-id="/complete.xhtml" 
                            redirect="true">
      <end-conversation />
   </page>
   
</pageflow-definition>

You can automatically generate the above XML file using a visual pageflow editor in the jBPM IDE, shown in Figure 2. For each state, there is a corresponding page with a view-id linking to the actual resource and all available transitions. From the "browse" page, you have two possible transitions: "browse" when a user update his shopping cart and "checkout" when she clicks on the checkout button. From the "checkout" page, she can modify the number of articles in her cart then go back to the checkout page or complete her order. Each action method will return the name of the transition to pass. Once the order is made, the conversation ends and all of the objects stored in that conversation are freed.

Figure 2
Figure 2. The DVD store jBPM pageflow designer in the jBPM IDE

To see how nested conversations work, you can watch and study the "issues" example available in the project CVS.

Business Process Integration

jBPM is used to define a flow of pages, but Seam can also help you to integrate business processes defined using the jBPM Process Definition Language (JPDL).

You won't have to deal with the jBPM API; you only need to provide a JPDL descriptor (which can be built using the jBPM IDE) and annotate the method that will create and start a new process instance. The business process integration offers a new context, called the business process context. Anything added to a process instance will live during the whole instance. Your business process can drive your method calls. This lets the process transitions trigger some method calls.

In the following example for the DVD store application, we add business process to the validation of any order. Let's add a business process that automatically accepts every order under 100 dollars and delegates validation to an administrator for all other orders. Once the validation is made, a process step is needed to add the shipping number information. To do such a process, we decided to write the following JPDL:

<process-definition name="OrderManagement">
    <start-state>
        <transition to="decide"/>
    </start-state>
    
    <decision name="decide" 
expression="#{orderApproval.howLargeIsOrder}">
        <transition name="large order" 
                       to="approval"/>
        <transition name="small order" 
                       to="process"/>
    </decision>

    <task-node name="approval">
        <task name="approve">
           <assignment 
             pooled-actors="reviewers" />
        </task>
        <transition name="approve" 
                       to="process"/>
        <transition name="reject"  
                       to="complete"/>
    </task-node>
    
    <task-node name="process">
        <task name="ship">
           <assignment pooled-actors=
           "#{shipperAssignment.pooledActors}"/>
        </task>
        <transition name="shipped" 
                       to="complete">
            <action expression=
                   "#{afterShipping.log}"/>
        </transition>
    </task-node>
    
    <end-state name="complete"/>
</process-definition>

In this simple but yet powerful example, we see how the integration of jBPM and Seam let you directly enter JSF expressions directly into the jPDL. The #{orderApproval.howLargeIsOrder} calls the howLargeIsOrder method on the "orderApproval" Seam component, which is exactly how it works in the JSF page. Here is the source code for that Seam component:

package com.jboss.dvd.seam;

import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;

@Name("orderApproval")
public class OrderApprovalDecision {
   @In float amount;
   public String getHowLargeIsOrder()
   {
      return amount > 100 ? 
        "large order" : "small order";
   }
}

Nothing fancy here; we just injected the amount from the stateful contexts and a string returns indicating which decision to take. We can also notice in the pageflow definition that the transition from "process" to "complete" triggers a call to the "log" method of the "afterShipping" Seam component.

Portable and Lightweight

Seam relies exclusively on JDK 5.0 annotation metadata for the declaration of components and how they are associated in a particular context--no XML hell. Seam can be used with any JSF implementation and runs on any JEE 5.0 container. With the Microcontainer, Seam can even be used in Tomcat or in unit tests. For developers who are not ready yet for EJB 3.0, Seam also supports POJOs and Hibernate persistent classes as components.

The DVD store example can work in Tomcat by adding few configuration files; there is such an example in the Seam distribution.

Resources

Thomas Heute is the project leader of JBoss Seam


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.