Lightweight services are long-running, server-side programs developed in lightweight programming languages such as JavaScript. High-level, script-friendly APIs provide access to Web services, Sametime instant messaging, and high-performance publish/subscribe. LWS is especially suited to developing personal services, which are server-side applications that operate on behalf of individual users. The term agent is used to refer to this type of service, and agent instance to refer to a running copy of an agent's program code.
This article shows how to extend a conventional J2EE application to include lightweight services and how to create and manage an agent instance for each user of the system. The example service will demonstrate the following features:
- Asynchronous Web service operations
- Web service operations, especially those that span organizations or enterprises, might take a significant amount of time to return a response or fail. LWS allows agents to dispatch multiple requests simultaneously, or process other events while waiting for an operation to complete. To facilitate this, LWS agents initiate Web service requests with an asynchronous API, and process the responses in separate transactions.
- Self-scheduling
- LWS agents are long-running because, like entity beans, their program state is persisted in a database. Script programs have the additional ability to schedule their own execution, independent of events outside the WebSphere container. This article demonstrates an agent that invokes a Web service operation periodically, with a configurable time interval.
- Managed access to non-transactional middleware
- As discussed in Part 1, LWS helps manage the interaction between transactional agents and non-transaction middleware resources. I'll create an agent that sends instant messages to users through Lotus Sametime.
- Agent style programming
- For both simplicity and security, I'll develop an agent program that only manages information for a single person. The example application will then create and configure an instance of this agent for each user. This is an alternative to developing code that explicitly manages data and events relating to multiple users.
In the following sections, I'll describe an application scenario, develop and debug a new LWS agent, and create an enterprise application for managing agent instances. See IBM Lightweight Services on IBM's alphaWorks site for additional tutorials, example agents, and script API documentation. In this article, I assume you are familiar with developing and testing J2EE enterprise applications in Application Developer. "Application Developer" in this article refers to WebSphere Studio Application Developer 4.0.3; the specific tasks discussed may vary for other versions of the development environment.
Personal Weather Service application
To demonstrate the features outlined above, the agent in the following examples should invoke asynchronous Web service operations, schedule its own execution, communicate by Sametime instant messaging, and use per-instance configuration values. Consider a fictitious Personal Weather Service Web application, which includes the option of regional temperature updates to be sent by instant messaging. A user can sign on, enter their ZIP code, and enable temperature updates sent at a personalized time interval. Figure 1 shows the Web interface for this application, which will be implemented in this article.
Figure 1. Personal Weather Service Web interface

Sametime instant messaging provides a convenient way to communicate directly with an end user. A temperature update will appear as a new message window in the Sametime Connect client application, as shown in Figure 2.
Figure 2. Temperature update message

The example application consists of four main server components:
- WebSphere Application Server
- DB2 Universal Database
- Lotus Sametime Server
- A temperature Web service accessible with HTTP
WebSphere and DB2 will host both the weather service enterprise application and LWS. Figure 3 shows the communication links between these systems and the client.
Figure 3. System overview

Figure 4 below shows the components hosted within the WebSphere server. The weather service enterprise application and LWS, while hosted within the same WebSphere instance, are installed separately and store their data in two different DB2 databases. The Web interface above will be implemented using JavaServer Pages (JSP) technology, and will invoke a user manager component, implemented as an Enterprise JavaBeans (EJB) session bean. The user manager will store user profile information in an entity bean, and invoke the LWS agent manager with Java Remote Method Invocation (RMI). You could also implement the cross-application link between the user manager and the agent manager using Web service requests over HTTP, but using RMI allows WebSphere transactions to span the two components.
All operations for creating, managing, and removing agent instances are performed through the agent manager session bean. Each time a new user registers for temperature updates, the user manager will create and configure a new instance of a temperature update agent. That agent instance will connect to the Sametime server and invoke the temperature Web service on the schedule specified for its assigned user. When the Web service request completes, the result is forwarded to the user in a new instant message.
Figure 4. WebSphere enterprise application using lightweight services

The next section explains how to configure LWS to run in WebSphere Studio Application Developer's WebSphere Test Environment. You will then learn how to develop and test the LWS agent in the LWS perspective, then implement the weather service enterprise application.
Importing LWS into Application Developer
When you develop a new J2EE application that uses lightweight services, it is convenient to run LWS within the WebSphere Test Environment included in WebSphere Studio Application Developer 4.0.3. The LWS installation instructions describe how to install the enterprise application on a stand-alone instance of WebSphere. In this case, you'd like to create a server configuration within Application Developer that includes both LWS and the new weather service application.
First, use Application Developer's File/Import... command to add LWS to the workspace. Select the enterprise application archive (EAR) file from the LWS install package, as shown in Figure 5, and name the new application project LightweightServices. This will also add several additional projects to the workspace -- the EJB and Web modules that define the agent manager, runtimes, and extensions.
Figure 5. Importing the LWS enterprise application

Once the LWS projects have been added to the workspace, you must configure the WebSphere Test Environment to host the application code. The configuration steps mirror those in the normal installation instructions, but are done using the Application Developer server perspective. You start with the Create Server Instance and Configuration command. Because this new server will be used to test the weather service application, call it Personal Weather Service and specify its project folder as PersonalWeatherService Server, as shown in Figure 6. The new server will now appear in the Server Configuration view under both Server Instances and Server Configurations.
Figure 6. Creating a new server instance and configuration

With the Environment tab of the server instance editor, add the system property com.ibm.ws.classloader.ejbDelegationMode with value false. This property allows EJB modules to override Java classes in the WebSphere classpath when they require a different version.
With the General tab of the server configuration editor, set module visibility to SERVER. This will allow the new weather service application to access EJB beans, which are part of the LWS application.
With the Data source tab, you need to configure access to the LWS database. If this database does not already exist, create it as specified in the LWS installation instructions. If you've already been using a database with a standard LWS installation, you can simply shut down that application server and use the same database with the WebSphere Test Environment.
Before creating a new data source, you need to specify which Java Database Connectivity (JDBC) driver class is used to communicate with DB2. The WebSphere configuration described in the overview contains two enterprise applications -- the weather service application and LWS. Each application stores information in its own DB2 database, so transactions that span both require a two phase commit protocol. This is not supported by the default DB2 driver, so edit Db2JdbcDriver and change the implementation class name to COM.ibm.db2.jdbc.DB2XADataSource, as shown in Figure 7. If you'll be accessing a newly created LWS database with this "JTA-enabled" DB2 driver, you'll first need to bind the db2cli and db2ubind packages. For more information, see Using JTA Drivers in the WebSphere 4.0 InfoCenter.
Figure 7. Reconfiguring the JDBC driver for DB2

Now that the DB2 driver has been reconfigured, you can create the data source required to store agents and their associated data, as shown in Figure 8. Add a new data source called AgentManagerDataSource with the Java Naming and Directory Interface (JNDI) name jdbc/AgentManager. Specify the database name LWS and enter your DB2 administrator user ID and password.
Figure 8. Creating DB2 data source for LightweightServices application

Finally, add the LightweightServices project to the Personal Weather Service server configuration. The test environment should now be configured. Start the new server and verify that LWS loads properly. The WebSphere Test Environment uses a different default port than a standard WebSphere installation; the LWS Web administration interface will now be at http://localhost:8080/lws/admin/web/.
In the LWS perspective, you now need to specify server localhost and port 8080 for installing and debugging agents.
Developing the temperature agent
The LWS perspective for Application Developer lets you develop and fully test the temperature agent before creating any of the other application components. Once you're confident that the agent is working properly, you can develop EJB components that create, configure, and remove instances of the temperature agent automatically.
Start the process by opening the LWS perspective and creating a new LWS project called TemperatureAgent, as shown in Figure 9.
Figure 9. Creating a new LWS project

As shown in Figure 10, specify the Rhino runtime, which is based on the Mozilla Rhino JavaScript engine. Be sure to consult the "Rhino Runtime" section of the scripting reference in the LWS documentation, as the two default runtimes have a number of API differences.
Figure 10. Specifying LWS project attributes

The TemperatureAgent.aml file that is created represents most information about the agent and the runtime environment it requires. The agent editor automatically opens this file to the Overview tab, as shown in Figure 11, where you can specify the basic properties of an agent. A unique agent ID is automatically assigned to the project, which you will use later when referring to your agent from the temperature service application. Customize the Author and Description fields to help identify your agent on the server.
Figure 11. Specifying agent properties

With the Extensions tab, enable the Sametime and WebServices extensions as shown in Figure 12. This will make the corresponding extension objects available as top-level properties in your JavaScript program. Note that the WebServices extension includes both client-side and server-side function; this agent will only act as a Web services client.
Figure 12. Adding extension dependencies

Before writing any JavaScript code, you want to consider what configuration information an agent instance might require at runtime. Some values, such as a ZIP code to use with the temperature service, will be set on a per-user basis. Other configuration values, such as Sametime login information, will be set by the developer of the weather service enterprise application. All these values are represented in the instance configuration XML, which you specify when creating a new agent instance. See the "Configuring Agent Instances" tutorial in the LWS documentation for more information on how the XML is translated to a JavaScript object.
To help streamline the development process, you can store a default value for the instance configuration XML as part of your project. With the Configuration tab in Figure 12, specify the XML fragment in Listing 1 as the default instance configuration. You should modify the information in the sametime tag to specify a Sametime server on your network, and make sure that both userId tags denote valid users on that server.
Listing 1. Default instance configuration
<configuration>
<userId>John Smith</userId>
<zipCode>02139</zipCode>
<period type="int">20000</period>
<retry type="int">20000</retry>
<sametime>
<server>localhost</server>
<userId>Temperature Agent</userId>
<password>temperatureagent</password>
</sametime>
</configuration>
|
Now you're ready to develop the agent's JavaScript source code. Open the TemperatureAgent.js file that was automatically created as part of your project. It will already contain a line of JavaScript to write a "hello world" message to the LWS perspective's Instance Log view.
Listing 2. Default JavaScript source code
Agent.Log.writeMessage("Hello world.");
|
The Install tab shown in Figure 13 lets you very quickly install and debug new versions of the agent source code. If you install and create a new instance of the default agent code, a new entry should appear in the Agents view, and the sample output should appear in the Instance Log view. You should also become familiar with the Command Line view, which allows JavaScript expressions to be evaluated in the scope of the server-side agent instance. If you enter another Agent.Log.writeMessage call in the command line, the output should again appear in the Instance Log view.
Figure 13. Testing the default JavaScript code

Now that you know that the development environment is properly configured, you can customize the agent source code. Start by removing the "hello world" command and defining convenience variables for accessing the instance configuration and log objects.
Listing 3. Instance configuration and log
var config = Agent.configuration; var log = Agent.Log; |
Your agent needs to invoke a temperature Web service hosted on another server, and the easiest way to do this is with the aid of a Web Services Description Language (WSDL) file. You'll be calling the temperature Web service described by http://www.xmethods.net/sd/2001/TemperatureService.wsdl
Instances of your agent will require access to this WSDL resource to generate valid requests. You could simply specify the URL for this information, but to simplify debugging, include the file as part of your agent. Copy the file into your agent project folder, making sure to "Refresh From Local" if you create the file outside of Application Developer. Once the WSDL file is in the agent's folder, the Refresh button on the Install tab should cause a new item to show up in the files list.
Now that the WSDL file is available as part of the agent, you can create a service object using the WebServices.WSDL.createService function. Define the getTemperature function as in Listing 4, which creates a getTemp request and sets the zipCode parameter to that specified in the instance configuration. All Web service requests in LWS are asynchronous, which means you must specify an event handler to process the result. The onGetTemperatureComplete function will be called when the operation has completed, whether the request failed or returned a result. For now, simply write the result to the instance log.
Listing 4. Using WSDL to invoke a Web service
var temperatureService =
WebServices.WSDL.createService("TemperatureService.wsdl");
function getTemperature() {
var request = temperatureService.createRequest("getTemp");
request.body.zipcode = config.zipCode;
request.onComplete = onGetTemperatureComplete;
log.writeMessage("Calling temperature service...");
request.call();
}
function onGetTemperatureComplete(response) {
if (response.failed) {
log.writeError("Call to temperature service failed. " + response.error);
return;
}
var temperature = response.result.value;
log.writeMessage("Temperature service returned " + temperature + ".");
}
|
Use the Install button (Figure 13) to re-install the agent and create a new agent instance. Make sure you've enabled the Remove all instances before install checkbox, which will remove the instance you created previously. In the Command Line view, manually invoke the Web service by typing getTemperature() and pressing return. Watch the Instance Log view for the result of the call.
Once the basic Web service call has been tested, you can add JavaScript code for integrating with a Sametime instant messaging server. The source code in Listing 5 creates a new top level JavaScript property "session" and sets it to a new Sametime session object. The session is assigned two event handlers that you'll use to monitor login attempts, and the login call creates a new connection using information provided in the instance configuration XML.
Listing 5. Integrating with Sametime instant messaging
log.writeMessage("Connecting to Sametime server...");
var session = new Sametime.Session();
session.onLogin = onLogin;
session.onLoginFailed = onLoginFailed;
session.login(
config.sametime.server,
config.sametime.userId,
config.sametime.password,
true);
function onLogin(session, reconnect) {
log.writeMessage("Logged in to Sametime server.");
}
function onLoginFailed(session, reasonCode, description) {
log.writeError("Sametime login failed. " + description);
}
|
When you create another new agent instance, it will immediately attempt to access the Sametime server. Watch the Instance Log view for the messages output by the onLogin or onLoginFailed events. Once the agent instance has successfully connected to Sametime, you can use the Command Line view to access the session property and send a message to a user. For example, if you were logged in as John Smith with the Sametime Connect client, you might evaluate the following command in the scope of your agent instance:
? session.sendText("John Smith", "This is a test.")
|
It's important to note that Sametime connections do not remain valid after the WebSphere server is restarted. Agent instances will still maintain references to Sametime session objects, but network operations on those objects would initially fail. LWS provides the Agent.onResume event so that agent instances may restore this type of non-persistent resource. In the case of Sametime sessions, all you need to do is call the login function with no arguments. The session object will use the same server, user ID, password, and event-handler information to create a new instant messaging connection.
Listing 6. Handling server restarts
Agent.onResume = onResume;
function onResume() {
session.login();
}
|
Once both the Web service request and basic Sametime connectivity have been tested, you can automatically send the result of the getTemp operation to the Sametime user specified in the instance configuration. Note that the getTemperature function now assumes that the top level session property is set to an active Sametime connection.
Listing 7. Sending the Web service response as a Sametime message
function onGetTemperatureComplete(response) {
if (response.failed) {
log.writeError("Call to temperature service failed. " + response.error);
return;
}
var temperature = response.result.value;
log.writeMessage("Temperature service returned " + temperature + ".");
session.sendText( |
Now that the getTemperature function invokes a Web service and sends the result as a Sametime message, you can invoke that function on a schedule. First, define a new top level JavaScript property called "timer," initially set to null. You will start the first Web service request once the agent instance has successfully logged in to Sametime. The onLogin function can be invoked multiple times as a result of automatic reconnects, which you requested by specifying true as the last parameter to the login function.
Add a clause to the onLogin event-handler that creates a new timer object only if it does not already exist. Timers are persistent, or remain valid after the WebSphere server is restarted. You won't start the timer yet, but set the getTemperature function as its event-handler and start the first temperature request immediately. In the first call to the onGetTemperatureComplete function, you'll kick off the cycle of Web service requests by starting the timer. Whenever a request fails, schedule the next call to getTemperature in the number of milliseconds specified by the retry instance configuration element. If a request succeeds, schedule the next call as specified by the period configuration element.
Listing 8. Scheduling requests with a timer object
var timer = null;
function onLogin(session, reconnect) {
log.writeMessage("Logged in to Sametime server.");
if (timer == null) { |
You now have a fully functional agent, which you can instantiate and debug from the LWS perspective. The next step is to create a new enterprise application that tracks user preferences, such as ZIP code, and automatically creates new agent instances on their behalf.
Developing the enterprise application
Start in Application Developer's J2EE perspective by creating a new enterprise application called PersonalWeatherService, as shown in Figure 14. This process also lets you create corresponding EJB and Web projects, which will later contain your EJB components and JSP Web interface. To develop and test the application incrementally, you'll begin by defining the UserProfile entity bean, then add the UserManager session bean, and finally define the JSP file that provides access to end users.
Figure 14. New enterprise application dialog

Figure 15 shows creating the UserProfile entity bean in the J2EE persective. Note that the bean's persistent fields store the configuration data for agent instances that you wish to specify on a per-user basis. Each user will have control over their ZIP code and the amount of time between temperature updates (period), and the unique field userId is assumed to be the same as their Sametime ID. You will use the instanceGroupId and instanceId fields to keep track of the agent instance created for each user.
Figure 15. Creating the UserProfile entity bean


Use the J2EE perspective to create a "data class" access bean called UserProfileData for your entity bean. If a remote method returns a reference to an entity bean, its client must then make a Java RMI call for each property it wishes to retrieve. Using a data class lets you pass copies of user profile information between application components, reading or updating all of an entity bean's fields in a single call. Application Developer will also create a new Java class called UserProfileFactory, which simplifies the creation and look-up of UserProfile instances.
Instances of the UserProfile entity bean will be stored in DB2, so you need to create an EJB to relational database (RDB) mapping. As shown in Figure 16, specify target database DB2 UDB V7.2, database name WEATHER, and schema name USERPROFILE. This process will also generate a database definition language (DDL) file, which will aid in setting up the required database table.
Figure 16. Creating the EJB to RDB mapping

After generating the deploy and RMIC code, you're ready to create the corresponding DB2 database and test the bean in the WebSphere test environment. Listing 9 is the transcript from a DB2 command window session where you create the WEATHER database. Note that you load the Table.ddl file generated in a previous step in order to initialize the new database. Also, make sure that the bind commands contain the correct path for your local DB2 installation. You can automate this process by using a batch file or shell script.
Listing 9. DB2 command window
C:\PROGRA~1\IBM\SQLLIB\BIN>db2 ATTACH TO DB2 USER DB2ADMIN
Enter current password for DB2ADMIN:
Instance Attachment Information
Instance server = DB2/NT 7.2.4
Authorization ID = DB2ADMIN
Local instance alias = DB2
C:\PROGRA~1\IBM\SQLLIB\BIN>db2 CREATE DATABASE WEATHER
DB20000I The CREATE DATABASE command completed successfully.
C:\PROGRA~1\IBM\SQLLIB\BIN>db2 CONNECT TO WEATHER USER DB2ADMIN
Enter current password for DB2ADMIN:
Database Connection Information
Database server = DB2/NT 7.2.4
SQL authorization ID = DB2ADMIN
Local database alias = WEATHER
C:\PROGRA~1\IBM\SQLLIB\BIN>db2 -t -f"C:\Program Files\IBM\
Application Developer\workspace\PersonalWeatherServiceEJB\ejbModule\
META-INF\Table.ddl"
DB20000I The SQL command completed successfully.
DB20000I The SQL command completed successfully.
DB20000I The SQL command completed successfully.
C:\PROGRA~1\IBM\SQLLIB\BIN>db2 BIND C:\Progra~1\SQLLIB\bnd\@db2cli.lst
LINE MESSAGES FOR db2cli.lst
------ --------------------------------------------------------------------
SQL0061W The binder is in progress.
SQL0091N Binding was ended with "0" errors and "0" warnings.
C:\PROGRA~1\IBM\SQLLIB\BIN>db2 BIND C:\Progra~1\SQLLIB\bnd\@db2ubind.lst
LINE MESSAGES FOR db2ubind.lst
------ --------------------------------------------------------------------
SQL0061W The binder is in progress.
LINE MESSAGES FOR db2clpnc.bnd
------ --------------------------------------------------------------------
SQL0595W Isolation level "NC" has been escalated to "UR".
SQLSTATE=01526
LINE MESSAGES FOR db2arxnc.bnd
------ --------------------------------------------------------------------
SQL0595W Isolation level "NC" has been escalated to "UR".
SQLSTATE=01526
LINE MESSAGES FOR db2ubind.lst
------ --------------------------------------------------------------------
SQL0091N Binding was ended with "0" errors and "2" warnings.
C:\PROGRA~1\IBM\SQLLIB\BIN>db2 DISCONNECT CURRENT
DB20000I The SQL DISCONNECT command completed successfully.
|
To test your entity bean on the server, you'll need to bind it to a new data source. In the server perspective, open Personal Weather Service in the server configuration editor and create another DB2 data source called Weather. Specify the JNDI name as jdbc/Weather, the database name as WEATHER (which we created above), and specify your DB2 administrator user ID and password.
Figure 17. Creating DB2 data source for PersonalWeatherService application

In the J2EE perspective, open the PersonalWeatherServiceEJB module in the EJB extension editor. With the Bindings tab, set the JNDI name for the default data source to jdbc/Weather.
Now you're ready to add the PersonalWeatherService project to the Personal Weather Service server configuration and start the WebSphere Test Environment. Make sure the server starts without errors, and launch the EJB test client. Use the JNDI explorer to navigate to the entry for UserProfileHome, which lets you create, modify, and find instances of your new entity bean.
Figure 18. Creating a UserProfile in the EJB test client

With the UserProfile entity bean tested, you can move on to developing the UserManager session bean, as shown in Figure 19. This component will manage user profile information, as well as create and destroy instances of the temperature agent by invoking the LWS agent manager.
Figure 19. Creating the UserManager session bean

Define the remote interface in Listing 10 for the new session bean, which will let the JSP interface retrieve and update user profiles. Rather than using remote references to the actual entity bean, use the UserProfileData class, which enables you to pass a complete copy of the user profile information in a single Java RMI call. Also, define a basic exception class called UserManagerException for returning application-level errors to the JSP component.
Listing 10. Remote interface for UserManager session bean
public interface UserManager extends EJBObject {
public UserProfileData getUserProfile(String userId)
throws RemoteException, UserManagerException;
public void updateUserProfile(UserProfileData profileData)
throws RemoteException, UserManagerException;
}
|
Before providing the implementation of the session bean, you'll need to modify the Java build path for the PersonalWeatherServiceEJB project. Because you set the server visibility to SERVER in a previous step, all of the classes you need will be available when your code runs within WebSphere. When working in the J2EE perspective, however, some of the required classes must be explicitly added to the compile-time classpath.
To invoke the agent manager, you'll need access to the classes in the AgentManagerEJB project that was imported as part of the LWS enterprise application. You would normally create a link in the Projects tab of the Java build path editor, but importing an EJB module in Application Developer doesn't recreate the normal file structure of an EJB project. Instead, the imported classes are collected in a single JAR file. You'll need to use the Libraries tab with the Add JARs... command to specify /AgentManagerEJB/AgentManagerEJB.imported_classes.jar, as shown in Figure 20.
Figure 20. Adding imported EJB module to Java build path

You will also need Xerces to create the instance configuration XML. This is available to all enterprise applications running in WebSphere, but needs to be added to the Application Developer compile-time environment. In the Libraries tab of the Java build path editor, select the Add Variable... command. As shown in Figure 21, use the WAS_PLUGINDIR variable combined with the path lib/xerces.jar to make the Xerces APIs available.
Figure 21. Adding WebSphere library to Java build path

You're now ready to provide the implementation of getUserProfile and updateUserProfile in the file UserManagerBean.java. The first thing you should do is to configure access to JRas message logging by creating a trace logger. This provides a convenient, configurable way to capture exception information for debugging purposes.
Listing 11. JRas trace logger definition
static final RASTraceLogger traceLogger;
static {
Manager manager = Manager.getManager();
traceLogger =
manager.createRASTraceLogger(
"IBM",
"Lightweight Services",
"WeatherTutorial",
UserManagerBean.class.getName());
manager.addLoggerToGroup(traceLogger, "LWS_WeatherTutorial");
}
|
To simplify the implementation of UserManager clients, define the getUserProfile method so that it automatically creates a new UserProfile if the specified user is not found. Since you haven't specified any default values for the fields of a user profile, clients will be expected to interpret a null ZIP code as "unspecified," a zero period value as "updates disabled," and a null instance group ID as "no agent instance active."
In this example code, only a single, catch-all exception handler is provided. In practice, you would catch each exception explicitly and provide a more detailed message for the UserManagerException that is thrown. The example code does output specific exception information to the trace log, which you will enable when debugging your new session bean.
Listing 12. Implementation of getUserProfile
public UserProfileData getUserProfile(String userId)
throws UserManagerException {
try {
UserProfile profile = null;
UserProfileFactory factory = new UserProfileFactory();
try {
profile = factory.findByPrimaryKey(userId);
} catch (FinderException e) {
profile = factory.create(userId);
}
return profile.getUserProfileData();
} catch (Exception e) {
traceLogger.exception(RASITraceEvent.TYPE_ERROR_EXC, this, "getUserProfile", e);
throw new UserManagerException("Failed to get user profile.");
}
}
|
The updateUserProfile method is where you'll invoke the LWS agent manager to create and remove agent instances. When creating agent instances, you'll need a way to generate the instance configuration required to customize the JavaScript code. When working in the LWS perspective, you defined a default XML fragment for debugging the temperature agent. Your enterprise application will need to specify the appropriate configuration information for each user. Some information (such as ZIP code and period) is specified on a per-user basis, while other values, such as Sametime login information will be defined as part of the EJB deployment. In the Environment tab of the EJB editor, define the following environment entries for the UserManager bean:
Listing 13. Environment entries for UserManager session bean
| Name | Type | Value |
| Instance Group ID | String | PersonalWeatherService |
| Temperature Agent ID | String | Your agent ID, for example "600DAA62C8C1001613D1CBCCE0E564B6" |
| Sametime Server | String | Host name for your Sametime server, such as "localhost" |
| Agent User ID | String | Temperature Agent |
| Agent Password | String | Sametime password for Temperature Agent, such as "mypasswd" |
| Retry | Integer | 30000 (30 seconds) |
The createConfigXML method combines these environment entries with per-user configuration values to generate an XML fragment with all the configuration data required by your agent implementation. You can then use a utility method included in LWS to serialize the document to a string.
Listing 14. Creating instance configuration XML
String createConfigXML(UserProfile profile) throws Exception {
Context environment = (Context) (new InitialContext()).lookup("java:comp/env");
String sametimeServer = (String) environment.lookup("Sametime Server");
String userId = (String) environment.lookup("Agent User ID");
String password = (String) environment.lookup("Agent Password");
Integer retry = (Integer) environment.lookup("Retry");
Document document = new DocumentImpl();
Element configElement = document.createElement("configuration");
document.appendChild(configElement);
Element element = document.createElement("userId");
element.appendChild(document.createTextNode((String) profile.getPrimaryKey()));
configElement.appendChild(element);
...
return com.ibm.lws.server.Utility.serializeNode(configElement);
}
|
In another helper method called createTemperatureAgent, you acquire a reference to the AgentManager session bean and create a new instance of the temperature agent. Using a custom instance group ID is a convenient way to group your agent instances in the LWS administration interface. By storing this group ID and the instance ID in the user profile, you can locate and remove the instance later.
Listing 15. Creating an instance of the temperature agent
void createTemperatureAgent(UserProfile profile) throws Exception {
AgentManager agentManager = (new AgentManagerFactory()).create();
String configXML = createConfigXML(profile);
Context environment = (Context) (new InitialContext()).lookup("java:comp/env");
String temperatureAgentId = (String) environment.lookup("Temperature Agent ID");
String instanceGroupId = (String) environment.lookup("Instance Group ID");
int instanceId =
agentManager.createAndStartInstance(
temperatureAgentId,
instanceGroupId,
configXML);
profile.setInstanceGroupId(instanceGroupId);
profile.setInstanceId(instanceId);
}
|
The updateUserProfile method accepts user profile data previously retrieved using getUserProfile, and removes and/or creates a corresponding instance of the temperature agent. For simplicity, assume that any agent instance created with a previous version of the user profile is obsolete. If such an instance exists, you acquire a reference to the agent manager and remove it. You then update the actual user profile instance with the contents of the data object, and create a new agent instance if updates are enabled (if the period value is greater than zero).
Notice that setRollbackOnly is called to veto the current transaction if any exception occurs. This is important because you are catching application-level exceptions thrown by the EJB container and by the agent manager. If an agent's implementation throws a JavaScript error during instance initialization, for example, any changes to a user's profile are not committed to the database.
Listing 16. Implementation of updateUserProfile
public void updateUserProfile(UserProfileData profileData)
throws UserManagerException {
try {
String userId = (String) profileData.getPrimaryKey();
UserProfile profile = (new UserProfileFactory()).findByPrimaryKey(userId);
String instanceGroupId = profile.getInstanceGroupId();
if (instanceGroupId != null) {
try {
AgentManager agentManager = (new AgentManagerFactory()).create();
agentManager.removeInstance(instanceGroupId, profile.getInstanceId());
} catch (AgentManagerException e) {
profileData.setInstanceGroupId(null);
}
}
profile.setUserProfileData(profileData);
if (profileData.getPeriod() != 0) {
createTemperatureAgent(profile);
}
} catch (Exception e) {
mySessionCtx.setRollbackOnly();
traceLogger.exception(
RASITraceEvent.TYPE_ERROR_EXC,
this,
"updateUserProfile",
e);
throw new UserManagerException("Failed to update user profile.");
}
|
Open the PersonalWeatherServiceEJB in the EJB editor and navigate to the Transaction tab. Set all user manager methods to transaction type "Required," ensuring that any database access will be within the scope of a WebSphere transaction. You'll need to further specify database access in the Methods tab of the EJB extension editor. For the UserManager bean, set all methods to isolation level SERIALIZABLE (the most conservative setting). For the UserProfile bean, set all methods to access intent UPDATE and isolation level SERIALIZABLE. This access intent setting prevents potential DB2 deadlocks due to lock promotion. After generating the deploy and RMIC code, you're ready to test the new session bean on the server.
To see the information your application outputs to JRas, you need to enable the trace service. With the Trace tab of the server configuration editor, enable trace and set the trace string to com.ibm.lws.doc.tutorial.personalweather.*=all=enabled. When the WebSphere test environment is started, trace output will appear in the file WSAD_HOME\workspace\.metadata\.plugins\com.ibm.etools.websphere.tools\logs\trace.log .
Start the server and use the EJB test client to acquire an instance of the user manager. Try getting a user profile, setting the ZIP code and period values, and updating it to create a new agent instance. Use the Instances page of the LWS administration interface to monitor the agent instances that are created. By opening an instance log from the Instances page, which is similar to the log in the LWS perspective, you can monitor your debugging messages, as well as any JavaScript errors that might occur.
Creating the JSP Web interface
The final component of the application is the JSP Web interface that will invoke the user manager on behalf of individual users. For simplicity, you can create a single JSP file that acts as the sign-in, profile editor, and response pages. You'll use a form-based sign-in procedure (ignoring the password field for now), which stores the user ID as a session attribute.
Start by acquiring a reference to the user manager home and creating a reference to the user manager session bean. The JSP component's only interaction with user profiles or agent instances is through the methods of this bean's remote interface. For the JSP component to compile, you'll need to import the package com.ibm.lws.doc.tutorial.personalweather and edit your Web project's module dependencies to include PersonalWeatherServiceEJB.
Listing 17. Acquiring UserManager session bean
InitialContext initialContext = new InitialContext();
Object homeObject =
initialContext.lookup(
"ejb/com/ibm/lws/doc/tutorial/personalweather/UserManagerHome");
UserManagerHome home =
(UserManagerHome) PortableRemoteObject.narrow(
homeObject,
UserManagerHome.class);
UserManager userManager = home.create();
|
You can define a single block of code that handles all the operations you need to support: logging in, updating a user profile, and logging out. The rest of the page simply needs to generate a form with the existing profile data filled out or generate a login form if userId is not set. Please see the included project files for the rest of the JSP code.
Listing 18. JSP form processing
String userId = (String) session.getAttribute("userId");
String action = request.getParameter("action");
if (action != null) {
if (action.equalsIgnoreCase("login")) {
userId = request.getParameter("userId");
if (userId.length() == 0) {
userId = null;
}
session.setAttribute("userId", userId);
} else if (action.equalsIgnoreCase("update") &&
request.getMethod().equalsIgnoreCase("POST")) {
UserProfileData profileData = userManager.getUserProfile(userId);
profileData.setZipCode(request.getParameter("zipCode"));
if (request.getParameter("enable") != null) {
profileData.setPeriod(Integer.parseInt(request.getParameter("period")));
} else {
profileData.setPeriod(0);
}
userManager.updateUserProfile(profileData);
} else if (action.equalsIgnoreCase("logout")) {
session.removeAttribute("userId");
userId = null;
}
}
|
With the JSP interface defined, you've now created a complete, end-to-end enterprise application including lightweight services. You can again use the LWS administration interface to monitor the agent instances created by the user manager. Start the server, log in to Sametime, and use the JSP interface to configure your temperature updates at http://localhost:8080/PersonalWeatherServiceWeb/.
Try creating user profiles for several different users, each with different update preferences.
Using a basic example application, I've shown how to extend a J2EE application with IBM Lightweight Services. This section outlines some alternatives, or additional features, you might want to try.
- Accumulate results
- LWS makes it easy to add new persistent JavaScript properties. You could store one or more Web service results, then use that information to enhance new temperature updates, such as "The temperature is 80º F, today's average is 78º F."
- Preserve agent instances
-
For simplicity, the example application removes a user's existing agent instance any time it needs to be reconfigured. It's also quite practical to reconfigure an existing agent instance using Web services. You could add
setZipCodeandsetPeriodWeb service operations to your agent, and invoke these operations from the user manager session bean. You might also choose to remove the ZIP code and period from the user profile entity bean, retrieving these values from an existing agent instance with additional Web service operations. - User status tracking
- The temperature agent attempts to send an update message any time the Web service operation completes. We make no attempt to determine if the target user is actually online, or if sending the message succeeded. Use the session object's
addUsersfunction and correspondingonUserStatusChangedevent to track when a user is online and available for instant messaging. The agent could delay the Web service call if the user is not available to receive the result. Handle theonImOpenFailedevent to be notified when sending a message has failed. - Use publish/subscribe
- Rather than call a temperature Web service on a set schedule, you could use the DistHub extension to subscribe to a temperature data feed. This would let you efficiently program more intelligent alerts, such as sending an update every time the temperature changes more than five degrees.
- Use alternative alert mechanisms
- In this example, we sent our updates with Sametime instant messaging because it's a relatively simple way to communicate directly with an end user. You might dispatch the same alerts by calling the Web service interface for a pager gateway or e-mail server.
- Call agent manager Web service
- In the example application, we ran the agent manager and the enterprise application in the same deployment of WebSphere. Calling the agent manager with Java RMI let us use distributed transactions when creating and removing agent instances. The agent manager is also available as a Web service, which may be more appropriate in a distributed or cross-enterprise application.
In this article, I've used IBM Lightweight Services to extend a J2EE application with asynchronous Web services, self-scheduling, and Sametime instant messaging. I was able to implement a relatively complicated process using a configurable agent, developed in less than 50 lines of JavaScript code. I hope this simple example shows how lightweight services can coordinate more complicated processes, managing sequences of Web service operations and interacting with individual users. Please see IBM Lightweight Services on IBM's alphaWorks site for frequently asked questions, scripting tutorials, and example agents.
| Name | Size | Download method |
|---|---|---|
| i-lws2.zip | HTTP |
Information about download methods
- Download a zipped file with the source code for this article.
- Read Part 1 of this series, "Server-side scripting".
- Learn about IBM Lightweight Services on alphaWorks.
- Read "Developing and Testing a Complete "Hello World" J2EE Application with WSAD" (WebSphere Developer Domain, October 2001).
- Refer to the ECMAScript Language Specification.
- Visit Sun's site for information about Java 2 Platform, Enterprise Edition.
- Get technical information about developing with Lotus Sametime from Lotus Developer Domain.
- Get information on WebSphere Application Server.
- Learn more about WebSphere Studio Application Developer or download the trial version of WebSphere Studio Application Developer Version 4.0.3. And visit WebSphere Developer Domain while you're looking!
Christopher Vincent is a software engineer on the Internet Technology Team, IBM Server Group. His research interests include instant messaging, publish/subscribe infrastructure, rapid application development, and dynamic programming languages. He received M.Eng. and B.S. degrees from MIT, where he worked at the MIT Artificial Intelligence Laboratory. You can contact him at crv@us.ibm.com.
