Enterprise Integration Zone is brought to you in partnership with:

Ryan Heaton is an engineer, architect, and consultant specializing the Web service design and development. Ryan has architected a wide variety of successful Web service applications and frameworks. One of his projects, Enunciate, has been released as a popular open-source Web service development framework. He has performed technical interviews for scores of candidates probing all aspects of Web service design and implementation. Ryan is a DZone MVB and is not an employee of DZone and has posted 9 posts at DZone. View Full User Profile

A Rich Web Service API for Your Favorite Framework, Part 2: JBoss Seam

11.18.2008
| 16928 views |
  • submit to reddit

The intent of this how-to series is to demonstrate the development of a rich Web service API on a variety of popular development frameworks. Part 2 (this tutorial) targets JBoss Seam.

  • REST endpoints. The messages example will supply REST resources in both XML and JSON data formats.
  • SOAP endpoints. The messages example API will be exposed via SOAP and defined by a well-defined and consolidated WSDL.
  • Full API Documentation. The messages Web service API will be fully-documented.
  • Client-side code. The application will provide client-side Java code that will be able to access the Web service API remotely.

Enunciate makes it drop-dead easy to develop this kind of Web service API: a few enhancements to the build file, a configuration file, a tweak to the web.xml deployment descriptor, and the definition of the service endpoint.

Step 1: Set up the Environment

It is assumed that you're able get the messages example working by following the instructions for running the examples on JBoss AS. So you should have JBoss AS installed as well as Ant 1.7 or later. Assuming JBoss is running, you should be able to navigate to the $SEAM_HOME/examples/messages directory (where $SEAM_HOME refers to the directory where you've unpacked the Seam distribution) and do:

ant deploy

You should be able to open up a browser to http://localhost:8080/seam-messages to see the messages application in all it's glory.

infoNote: This tutorial was written based on Seam 2.1.0.GA and JBoss AS 4.2.3.GA, both of which were the latest versions available at the time of writing.

We next need to integrate Enunciate with our project. Since we're using Ant, we have to download the latest Enunciate distribution and unpack it somewhere (we'll call this place $ENUNCIATE_HOME). We'll have to pass a reference to this location to our build environment by editing $SEAM_HOME/build.properties. You should have already edited this file to supply the jboss.home property as specified in the Seam tutorial. We now add another property: enunciate.home=/path/to/enunciate/home.

The final step for integrating Enunciate into our build environment is to edit the $SEAM_HOME/examples/message/build.xml file. We do some custom build magic to invoke Enunciate when building our war directory and adding some extra dependencies in our ear directory. We do this by overriding the default "war" and "ear" Ant targets. We won't go into too much detail here. All of this is documented here. But this is what the build file should look like when we're done (you also can download it here):

build.xml

<?xml version="1.0"?>

<project name="Messages" default="deploy" basedir=".">

<!-- Example name -->
<property name="Name" value="Seam Message List Example"/>
<property name="example.name" value="jboss-seam-messages"/>

<!-- Libraries -->
<property name="seam.ui.lib" value="yes"/>
<property name="tomcat.standard.tag" value="yes"/>

<import file="../build.xml"/>

<target name="copyextradependencies">
<copyInlineDependencies id="jstl" scope="runtime" todir="${lib.dir}">
<dependency groupId="apache-taglibs" artifactId="jstl" version="1.1.2"/>
</copyInlineDependencies>
</target>

<target name="war" depends="SeamExample.war">
<!--make sure enunciate.home is set.-->
<fail unless="enunciate.home">
enunciate.home not set, update build.properties
</fail>

<!--define the enunciate classpath-->
<path id="enunciate.classpath">
<fileset dir="${enunciate.home}/lib">
<include name="*.jar"/>
</fileset>
<fileset dir="${enunciate.home}">
<include name="enunciate-full-*.jar"/>
</fileset>
<fileset dir="${java.home}">
<include name="lib/tools.jar"/>
</fileset>
<path refid="build.classpath"/>
</path>

<!--define the enunciate task-->
<taskdef name="enunciate" classname="org.codehaus.enunciate.main.EnunciateTask">
<classpath refid="enunciate.classpath"/>
</taskdef>

<!--enunciate the api-->
<enunciate basedir="src" configFile="enunciate.xml">
<include name="**/*.java"/>
<classpath refid="enunciate.classpath"/>
<export artifactId="spring.app.dir" destination="${war.dir}"/>
</enunciate>

</target>

<target name="ear" depends="SeamExample.ear">
<!--copy the enunciate dependencies to the ear-->
<copy todir="${ear.dir}/lib">
<fileset dir="${enunciate.home}/lib">
<include name="*.jar"/>

<!--exclude annotations.jar since jboss ships with its
own jar containing the jsr250 annoations-->
<exclude name="annotations*.jar"/>

<!--exclude servlet.jar; provided by jboss-->
<exclude name="servlet*.jar"/>

<!--exclude stax; it ships with the JDK-->
<exclude name="stax*.jar"/>
</fileset>
<fileset dir="${enunciate.home}">
<include name="enunciate-full-*.jar"/>
</fileset>
</copy>

</target>
</project>

Now let's add a Web service API.

Step 2: Enunciate Configuration

Here is an Enunciate configuration file that Enunciate uses to expose the Web service API for the messages service. Drop that in the root of the messages application (next to the build.xml file). Let's briefly go over the basic parts of this file.

The <api-classes> element simply tells Enunciate what classes are to be used to define the Web service API. By default, Enunciate assumes all classes in the project are a part of the Web service API, but the messages application has other classes that are used to drive the UI that were never meant to be a part of a Web service API. So we have to tell Enunciate that only two classes are used to define our Web service API (note that we haven't defined the "MessageService" class yet):

enunciate.xml

...
<api-classes>
<include pattern="org.jboss.seam.example.messages.Message"/>
<include pattern="org.jboss.seam.example.messages.MessageService"/>
</api-classes>
...

The balance of the configuration file is used to define behavior in a specific Enunciate module. Enunciate generates the documentation of our Web service API from the JavaDocs of our API classes. By configuring the docs module, we tell Enunciate to put the documentation in the /api directory of the application and assign the documentation a title.

Enunciate assembles the Web service API in the form of a servlet-based application, and it needs to be merged with the messages application. We do this by merging the Enunciate-generated web.xml file with the messages web.xml file.

We also tell Enunciate to forget about compiling, packaging, and dependency-copying since the messages application build already does that.

enunciate.xml

...
<modules>
<docs docsDir="api" title="Seam Messages API"/>
<spring-app doCompile="false" doPackage="false" doLibCopy="false">
<war mergeWebXML="resources/WEB-INF/web.xml"/>
</spring-app>
</modules>
...

 

Step 3: Define the API Classes


Create MessageService.java

Seam (and JSF in general) uses a state-oriented style for defining business logic and process management. We've got to provide a service-oriented interface that can more easily be exposed as a Web service. We'll just write our own service to leverage the already-defined functionality of the MessageManagerBean. Our service will have to methods: getMessages to get a list of all messages and getMessage go get a specific message by id. In order to implement this service, we have to make a minor change to our session bean to expose the message list. We simply change the findMessages method to return the list of messages.

Here's what the org.jboss.seam.example.messages.MessageManager interface should look like (the only thing changed is the return value of the findMessages method):

MessageManager.java

@Local
public interface MessageManager
{
public List<Message> findMessages();
public void select();
public void delete();
public void destroy();
}

And here's the org.jboss.seam.example.messages.MessageManagerBean implementation bean:

MessageManagerBean.java

@Stateful
@Scope(SESSION)
@Name("messageManager")
public class MessageManagerBean implements Serializable, MessageManager
{

...

@Factory("messageList")
public List<Message> findMessages()
{
messageList = em.createQuery("select msg from Message msg order by msg.datetime desc").getResultList();
return messageList;
}

...

}

Now we can define a simple org.jboss.seam.example.messages.MessageService class that defines our operations (you can also download it here):

MessageService.java

public class MessageService {

public List<Message> getMessages() {
MessageManager bean = getMessageManager();
return bean.findMessages();
}

public Message getMessage(long id) {
for (Message message : getMessages()) {
if (message.getId() != null && message.getId().equals(id)) {
return message;
}
}

return null;
}

private MessageManager getMessageManager() {
return (MessageManager) Component.getInstance(MessageManagerBean.class, ScopeType.SESSION, true);
}
}

SOAP Metadata

To expose a SOAP interface we just need to apply some JAX-WS metadata to MessageService. Specifically, we apply the @javax.jws.WebService annotation to the class.

REST Metadata

Of course we also want to apply a REST interface to our API. This can be done by applying JAX-RS annotations to MessageService, but it's a bit more complicated because of the additional constraints of a REST API.

First of all, you have to map the service to a URI path by applying the @javax.ws.rs.Path annotation. We'll mount the service at the "/messages" path.

Next, since you're limited to a constrained set of operations, you have to annotate specific methods that are to be included in the REST API. You must specify the HTTP method that is used to invoke the method and the subpath that is used to locate it. We'll keep it simple by exposing just the getMessage method via the GET operation using the javax.ws.rs.GET annotation and mounting the method at the "/messages/message/{id}" path using the javax.ws.rs.Path annotation. The "{id}" on the path will specify the id of the message that we want to get. This means that the method parameter must be annotated with the @javax.ws.rs.PathParam annotation which is also used to specify the name of the path parameter.

Of course, you can expose other methods using other annotations, but we'll refer you to the JAX-RS documentation to learn how to do that. This is what MessageService looks like after all the annotations are applied:

MessageService.java

package org.jboss.seam.example.messages;

import org.jboss.seam.Component;
import org.jboss.seam.ScopeType;

import javax.jws.WebService;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import java.util.List;

/**
* Message service for retrieving messages.
*/
@WebService
@Path("/messages")
public class MessageService {

/**
* Get the list of messages.
*
* @return The list of messages.
*/
public List<Message> getMessages() {
MessageManager bean = getMessageManager();
return bean.findMessages();
}

/**
* Get the message of the specified id.
*
* @param id The message id.
* @return The message, if any.
*/
@GET
@Path("/message/{id}")
public Message getMessage(@PathParam("id") long id) {
for (Message message : getMessages()) {
if (message.getId() != null && message.getId().equals(id)) {
return message;
}
}

return null;
}

private MessageManager getMessageManager() {
return (MessageManager) Component.getInstance(MessageManagerBean.class, ScopeType.SESSION, true);
}
}

One more thing is required in order to expose a REST API. Since by default the REST endpoints will expose XML data, we have to provide root XML elements for the XML responses. To do this, we simply annotate the org.jboss.seam.example.messages.Message class with @javax.xml.bind.annotation.XmlRootElement.

Message.java

@XmlRootElement
public class Message {
...
}

 

Step 4: Establish the Seam Component Lifecycle

Since our MessageService takes advantage of the Seam component lifecycle (note the getMessageManager method), we need to make sure that the lifecycle is set up when our service is called. We do this by leveraging a ServletFilter that has already been written by the Seam folks. We enable this filter in our resources/WEB-INF/web.xml file and apply it to all requests:

<filter>
<filter-name>Seam Context Filter</filter-name>
<filter-class>org.jboss.seam.web.ContextFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>Seam Context Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

 

Step 5: Build and Deploy

Back on the command-line:

ant deploy

 

Behold the Glory

Your application is fully-functional at http://localhost:8080/seam-messages/.

Seam Home

Check out the documentation for your new Web service API at http://localhost:8080/seam-messages/api/:

Seam API Home

Everything is documented, scraped from the JavaDoc comments. Here's the documentation for the SOAP API:

Seam SOAP Docs

And documentation for the REST API:

Seam SOAP Docs

And you can download client-side libraries that Enunciate generated and can be used to invoke your Web service API:

Seam Client Downloads

What about your WSDL? http://localhost:8080/seam-messages/api/ns1.wsdl

Seam WSDL

What about your XML-Schema? http://localhost:8080/seam-messages/api/ns0.xsd

Seam Schema

Want to see your API in action? Your SOAP endpoints are mounted at the /soap subcontext, and your REST endpoints are mounted at the /rest subcontext. To view a message, just use the path we defined with JAX-RS annotations relative to the /rest subcontext. So to view the message identified by id "1", we use http://localhost:8080/seam-messages/rest/messages/message/1:

Seam Example XML

As a convenience, the same XML resource can also be found at http://localhost:8080/seam-messages/rest/messages/message/1. And if you want to get that same resource as JSON, you can use http://localhost:8080/seam-messages/json/messages/message/1.

And Beyond...

Well, that's how easy it is to add a Web service API to you JBoss Seam application. But we've only barely scratched the surface of what Enunciate can do. What about any of this:

  • Security (HTTP Auth, OAuth, form-based login, session management, etc.)
  • GWT RPC endpoints and client-side JavaScript for accessing them.
  • AMF endpoints and client-side ActionScript for accessing them.
  • Streaming API for large requests.
  • Etc.

At this point, it's only a matter of configuration....

Published at DZone with permission of Ryan Heaton, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Kisito Momo replied on Mon, 2009/08/03 - 6:57am

i'm using seam 2.1.1, java1.6_12, JBoss4.2.3, Enunciate1.12 when i follow all the step describe here:
1) seam-message doesn't work: when i open up my browser to http://localhost:8080/seam-messages/messages.seam, i get this error: javax.servlet.ServletException: For input string: "rowCount" javax.faces.webapp.FacesServlet.service(FacesServlet.java:277) org.codehaus.enunciate.modules.spring_app.HTTPRequestContextFilter.doFilter(HTTPRequestContextFilter.java:36) org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:83) org.jboss.seam.web.LoggingFilter.doFilter(LoggingFilter.java:58) org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69) org.jboss.seam.web.IdentityFilter.doFilter(IdentityFilter.java:40) org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69) org.jboss.seam.web.MultipartFilter.doFilter(MultipartFilter.java:90) org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69) org.jboss.seam.web.ExceptionFilter.doFilter(ExceptionFilter.java:64) org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69) org.jboss.seam.web.RedirectFilter.doFilter(RedirectFilter.java:45) org.jboss.seam.servlet.SeamFilter$FilterChainImpl.doFilter(SeamFilter.java:69) org.jboss.seam.servlet.SeamFilter.doFilter(SeamFilter.java:158) org.jboss.web.tomcat.filters.ReplyHeaderFilter.doFilter(ReplyHeaderFilter.java:96)

2) The java1.5 client API doesn't work, not only for this example, but also for the Enunciate getting started example (IfYouWannaBeCool.com).

Please what's wrong? How to get the correct step, or the correct sample code that use the generated client API ?

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.