Skip to main content

Seamless JSF, Part 3: Ajax for JSF

Seam Remoting and Ajax4jsf seamlessly fuse client and server

Dan Allen (dan.allen@mojavelinux.com), Senior Java engineer, CodeRyte, Inc.
Dan Allen
Dan Allen is currently a senior Java engineer at CodeRyte, Inc. He is also a passionate open source advocate and gets a bit manic whenever he catches sight of a penguin. After graduating from Cornell University with a degree in Materials Science and Engineering, Dan became captivated by the world of Linux and open source software. He has been slaving over Web applications ever since, with the last several years focused exclusively on Java-related technologies, including Spring, Hibernate, Maven 2, and the abundant JSF stack. You can keep up with Dan's development experiences by subscribing to his blog at http://www.mojavelinux.com.

Summary:  JSF's component-based methodology encourages abstraction, but most Ajax implementations interfere with it by exposing the underlying HTTP exchange. In this final article in the Seamless JSF series, Dan Allen shows you how to use the Seam Remoting API and Ajax4jsf components to communicate with managed beans on the server as if they were local to the browser. You'll learn how surprisingly easy it is to leverage Ajax as a natural improvement on JSF's event-driven architecture and how to do so without compromising the JSF component model.

View more content in this series

Date:  12 Jun 2007
Level:  Intermediate
Activity:  12729 views

Most Java™ developers these days appreciate a good mashup, so you might be wondering how well Seam integrates with the technologies dubbed Web 2.0, especially Ajax. It would be pretty cool if you could use Seam to enable partial page updates in JSF or to facilitate a JSF application mashup with Google Maps, wouldn't it? Well, you can -- and better yet, it's easy.

In this final article in the Seamless JSF series, I show you how to use the Seam Remoting API and Ajax4jsf components to facilitate Ajax-style interactions in JSF-based applications. As you'll learn, the best thing about combining Seam with Ajax is that it lets you do all the fancy Web 2.0 stuff without getting tangled up in the JavaScript XMLHttpRequest object. With Seam Remoting and Ajax4jsf, you can communicate with managed beans on the server as if they were local to the browser. The browser and server state remain in sync, and you never have to deal with the low-level API that facilitates their communication.

I'll begin by showing you how Seam facilitates a new, component-based approach to Ajax programming. You will learn to use the Seam Remoting API to communicate between JavaScript and server-side objects via Ajax. Once you understand this new (and easier) approach to Ajax, you can use it to enhance the Open 18 application by doing the following:

  • Creating a mashup between the Open 18 course directory and Google Maps.
  • Using Ajax4jsf to merge the application's course directory page and course detail page.
  • Revisiting the application's Spring integration and making the Spring bean available during the Seam Remoting life cycle.

Upgrade to Seam 1.2.1.GA

Things are moving very quickly in the world of Seam, and a new version of the framework has been released since the last article in this series. The examples in this article require Seam 1.2.1 or later. In earlier versions of Seam, server-side remoting functionality was handled entirely by the SeamRemotingServlet. With the latest release, that functionality has been refactored out to the Seam Remoting package, and a generic ResourceServlet is used to delegate the handling of non-JSF requests such as remoting calls. Also, the remoting library is now packaged as a separate JAR, jboss-seam-remoting.jar. You need to include the JAR in your application's classpath, in addition to the list of required JARs that were identified to run the example in "Seamless JSF, Part 1: An application framework tailor-made for JSF."

The mashup between Open 18 and Google Maps enables users to plot locations in the golf course directory on a map. Merging the course directory and course detail pages (and Ajaxifying the underlying code) allows you to show course details without loading a new page. Integrating your Spring bean with Seam Remoting enables you to capture a repositioning of the Google Maps location marker and store the corresponding course latitude and longitude points in your database. The result, as you'll see, is an impressive Web 2.0-style application that any golf player would love to use!

If you've been burned by over-complicated, JavaScript-heavy Ajax programming in the past or if you've simply avoided Ajax so far because you don't want to deal with its complexity, what this article teaches you should vanquish your fears. You do some JavaScript coding as you refactor the application, but unlike most Ajax implementations, JavaScript won't be the bulk of your code; instead it merely extends your server-side Java objects.

A different way to Ajax

Just as you want to avoid explicit memory management in your applications, you do not want to have to deal with the low-level Ajax request protocols. Doing so is simply a big headache -- or multiple headaches, rather, with names like multibrowser support, data marshaling, concurrency violation, server load, and custom servlets and servlet filters. The biggest headache of all, and the one you really want to avoid, is the unintended exposure of the stateless, request-response paradigm that a component-based framework like JSF is intended to hide.

The JSF life cycle encourages strong component-oriented design by shielding application code from the underlying servlet model. To retain this abstraction when working with Ajax, you can hand off the low-level grunt work to either Seam Remoting or Ajax4jsf. Both of these libraries take care of the plumbing required to fuse JSF components to the browser via Ajax interaction. Figure 1 shows Seam Remoting in action. When an event is triggered, such as when a user clicks on a button, a message can be sent to the component on the server asynchronously. When the response is received, it can be used to incrementally update the page. The low-level communication protocols used to facilitate interaction between the browser and the server-side component are hidden behind the API.

About this series

Seamless JSF showcases Seam as the first application framework that truly fits JSF, compensating for its major weaknesses like no other extension framework does. Read the articles in this series and decide for yourself whether Seam is a worthy complement to JSF.

In the use case shown in Figure 1, the user is presented with the results of the method call that happens after he or she clicks a button. There are two important points to keep in mind when studying this use case: (1) the page is never refreshed, and (2) the client code communicates transparently with the method on the component, rather than explicitly building and then requesting a URL. A standard HTTP request is being used behind the scenes, of course, but the client code never has to interact with the HTTP protocol directly.


Figure 1. Seam Remoting fuses JSF component and browser
A use-case diagram for Seam Remoting

Seam Remoting and Ajax4jsf

Seam Remoting and Ajax4jsf are distinct libraries that each serve a purpose in the "Ajaxification" of JSF. Both libraries use Ajax to introduce an interaction model where communication between the browser and the server can happen asynchronously in the background, invisible to the user. After all, why waste the user's time with a page reload just to execute methods on the server? The information retrieved from the server in the Ajax requests made by these libraries can be used to incrementally update the state of the page in "real time." Both libraries instrument a life cycle that can restore the state of the components whenever they are needed by the browser. This Ajax interaction is not really a request but rather a "restore and execute." The browser is sort of tapping the server on the shoulder and asking it to execute a method on one of the server-side managed beans and return the result.

Although the two libraries work differently, they are not mutually exclusive. Because they both honor the JSF component model, they can easily be combined, as this article shows later. For now, just briefly consider each one's approach to incorporating Ajax-style interaction into JSF applications:

  • Seam Remoting provides JavaScript APIs that you can use to access server-side components in JavaScript as if they were local objects, to either send or retrieve data through method calls. Seam Remoting uses a custom, non-JSF life cycle to allow the browser to communicate with the server-side components. Only the Seam container and its components are restored during these requests. The transport protocol is Ajax, but you do not need to worry about the details of how the packets are transferred.

  • Ajax4jsf pushes the abstraction even further by completely hiding the use of JavaScript. It wraps all of the logic inside of basic UI components. Ajax4jsf takes the Ajax request through the complete JSF life cycle. Therefore, the Ajax-enabled components can execute action handlers on the server, update the JSF component tree, and re-render portions of the page, all without triggering a browser-navigation event. Again, the communication is done through Ajax, but it all happens under the covers, invisible to the page developer. Ajax4jsf's component-oriented approach makes Ajax functionality a natural part of JSF rather than a non-conforming outlander.

I dig into both of these approaches, but let's get some Ajax basics out of the way first.


Bridging the great divide

To make an application "rich" in the Ajax/Web 2.0 sense, the Web browser (otherwise known as the client) must be able to directly access components on the server. Making this vision a reality is challenging because of the rather significant chasm between client and server. On one side of this chasm (also known as the network) sits the client browser and on the other side sit the server and its components. The goal of an Ajax application is to get them talking to each other.

Actually, in most traditional Web applications, the client and server are already talking just fine, it's just that the interaction is completely one sided. The server talks, the browser listens. No doubt you've been stuck in those kinds of conversations before. In a world without Ajax communication, the browser can send a synchronous request for any URL, but it is then obliged to render whatever HTML the server sends back. Another disadvantage of this sort of interaction is that it often entails a lot of waiting.

With only the rudimentary language of HTTP in hand, the browser client remains completely naive about how the server generates HTML and therefore knows nothing of its components. From the standpoint of the browser, the page generation process is just a black box. The browser can ask the server different questions in the form of URLs and pass along hints packaged as request parameters and POST data, but it doesn't really speak the server's language. If you want to give the browser a view into your application's server-side activity, you need to establish a more sophisticated means of communication. The deterministic, page-oriented approach just doesn't cut it.

The Web browser as Ajax client

Seam Remoting and Ajax4jsf take distinctly different approaches to breaking the ice between client and browser components, so it's important to know how to leverage each one. Seam Remoting offers an API in the browser's native tongue, JavaScript, through which methods on server-side components can be accessed. For access to be granted to those methods, they must be explicitly marked as "remote" through the @WebRemote annotation.

The invocation mechanism in Seam Remoting parallels Java RMI by similarly allowing JavaScript to call methods on a component located on a remote server, using a local proxy object, or "stub." As far as the client is concerned, this stub object is the remote object. The stub is responsible for carrying out method execution on the actual remote object. When a remote method is called, the response encapsulates the return value of the method call. The return value is unmarshaled as a JavaScript object as it comes back across the wire. Hence, Seam Remoting enables the browser to speak the native tongue of the server. Java code and JavaScript have at last become one -- which may surprise those who still believe that they have been the same language all along!

Conversations with Ajax

Seam's approach to Ajax makes the browser privy to intricate details of the state of the server, such as which conversations are active. Your ability to use Seam Remoting to control the conversation context is important later on, when it comes time to make remote calls on conversation-scoped objects.

Ajax4jsf, on the other hand, provides JSF component tags for declaratively associating UI events with action handlers on server-side managed beans. These action handler methods do not need to be marked as "remote." Instead, they are traditional JSF action handlers, which are public methods on managed beans that either have no-arguments or accept an ActionEvent.

Unlike Seam Remoting, Ajax4jsf can communicate changes in the JSF component tree back to the browser. These changes are returned in the form of XHTML fragments. The fragments correlate to the individual JSF components on the page and manifest themselves as partial page updates. Thus, isolated areas of the page can be re-rendered by the browser using the new markup. These fragments are specifically requested, either by using the reRender attribute of the Ajax4jsf component tag or by wrapping regions of a template in an Ajax output panel marked with the attribute ajaxRendered="true". The reRender attribute indicates a specific set of components that should be re-rendered, referenced by their component IDs. In contrast, the use of ajaxRendered="true" is a blanket approach, instructing all "Ajax rendered" regions to be updated whenever an Ajax4jsf-managed Ajax request completes.

Both Ajax4jsf and Seam Remoting ripen the browser from a basic HTML renderer to a fully mature Ajax client. Applications really get exciting when you integrate these two frameworks. In fact (this next point could surprise you, so you might want to sit down for it), combining the capabilities of Seam Remoting and Ajax4jsf can free you from developing your own custom Ajax JSF components! You can enlist any existing, non-Ajax enabled JSF component in Ajax communication simply by nesting an a4j:support tag inside of its declaration. If you are working outside of a UI component (as you do later in the Google Maps mashup) and need to query a server-side component for information, update it, or instruct it to perform an operation, you can use Seam Remoting to manage the interaction.

While Seam Remoting and Ajax4jsf do have some functional overlap, both come in handy for adding Ajax-style interactions to your applications. Furthermore, as you'll soon see, the two libraries provide a seamless Ajax solution for your JSF applications.


Seam Remoting quickstart

You may be thinking that Seam Remoting sounds like an ideal solution if only it weren't so complex to implement. Fear not! Seam Remoting is nothing like those scary remote EJB objects you're probably thinking of. The best part about using the Seam Remoting API to enable JavaScript code to interact with server-side components is that the process is so unbelievably simple. Seam truly does all the hard work for you -- you don't even have to edit a single line of XML to start using it! (Which should be a relief if you're doing more XML programming than Java programming these days.)

Let's take a quick walk through the steps involved in using Seam Remoting to "Ajaxify" your JSF applications.

Expose your bean

There are just two requirements for exposing a server-side object method to a remote Ajax client: the method must be a public member of a Seam component and it must be outfitted with the @WebRemote annotation. That's it!

You can see this simplicity in action in Listing 1, where the Seam component ReasonPotAction exposes a single method, drawReason(), to an Ajax client for remote execution. Each time this method is invoked on the stub, the call is passed across the Internet to the server and randomly selects one of the listed "10 reasons to use Seam for your next project," using the corresponding method on the server-side component. The server then returns that value back across the wire to the client. (See Resources to find out more about those 10 reasons.)


Listing 1. Exposing a remote method call
                @Name("reasonPot")
@Scope(ScopeType.SESSION)
public class ReasonPotAction {
	private static String[] reasons = new String[] {
		"It's the quickest way to get \"rich\".",
		"It's the easiest way to get started with EJB 3.0.",
		"It's the best way to leverage JSF.",
		"It's the easiest way to do BPM.",
		"But CRUD is easy too!",
		"It makes persistence a breeze.",
		"Use annotations (instead of XML).",
		"Get hip to automated integration testing.",
		"Marry open source with open standards.",
		"It just works!"
	};
	
	private Random randomIndexSelector = new Random();
	
	@WebRemote
	public String drawReason() {
		return reasons[randomIndexSelector.nextInt(reasons.length)];
	}
}

Serve the resources

With the server-side component set up, you need to prepare the browser to invoke the @WebRemote method. Seam uses a custom servlet to handle HTTP requests to execute a remote method and return its result. No worries, though: You will not have to interact with that servlet directly. The Seam Remoting JavaScript library takes care of all interactions with the Seam servlet in the same way it manages all aspects of the XMLHttpRequest object.

The only time you even have to think about this servlet is when you are setting up Seam Remoting in your application. Better yet, you only need to configure Seam's custom servlet once, regardless of how many of your Seam features require the services of a custom servlet. Instead of using a specialized servlet for each feature that serves an asset to the browser (such as a JavaScript file) or processes a non-JSF request (like an Ajax remoting calls), Seam bundles these duties under the umbrella of a single controller, the Resource Servlet. This servlet uses a delegating chain model, passing on the duties to registered handlers. For instance, the Remoting object (which I discuss later) registers itself to receive all Ajax requests sent by the Seam Remoting JavaScript library.

The XML definition for the Resource Servlet, shown in Listing 2, must be installed in your application's web.xml file, typically below the Faces Servlet:


Listing 2. The XML definition for the Seam Resource Servlet
                <servlet>
  <servlet-name>Seam Resource Servlet</servlet-name>
  <servlet-class>org.jboss.seam.servlet.ResourceServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>Seam Resource Servlet</servlet-name>
  <url-pattern>/seam/resource/*</url-pattern>
</servlet-mapping>

Bootstrap the API

The real magic behind the Seam Remoting mechanism occurs in the JavaScript libraries. The Resource Servlet delegates work to the Remoting object to serve these libraries. The emitted JavaScript libraries, of which there are two, make the appropriate hooks available to the browser for invoking the remote methods via JavaScript.

You have to import both of the JavaScript libraries shown in Listing 3 into your page. The first library, remote.js, is static and ships Seam's client-side remoting framework to the browser. The second library, interface.js, is created dynamically on each request. It contains the remote stubs and complex types that are needed to interact with your server-side components. Note that Seam does not generate stubs and complex types for all components that specify a method as remote. Instead, it parses the query string following the ? symbol in the interface.js URL to collect the names of components to expose. Each component name is separated by an &amp; symbol. Using this stacked approach keeps the number of requests by the browser for external JavaScript files to a minimum. Seam then generates the stubs and complex types for only those components. As an example, the two JavaScript imports in Listing 3 load the Seam Remoting library and instruct Seam to prepare both the reasonPot and the anotherName component:


Listing 3. Importing the client-side framework and APIs
                <script type="text/javascript"
src="seam/resource/remoting/resource/remote.js"></script>
<script type="text/javascript"
src="seam/resource/remoting/interface.js?reasonPot&amp;anotherName"></script>

That's it -- you are ready to go! You've completed the Seam Remoting setup and it's time to start making some remote calls.


Making a remote call

All the client-side framework code for the Seam Remoting library is encapsulated in the Seam JavaScript object. Actually, this top-level object is simply a namespace -- a container that allows you to bundle a set of functionality under a unique name. In JavaScript, a namespace is nothing more than an object that holds static properties, static methods, and other nested namespace objects. The important thing to note is that the JavaScript in the Seam Remoting library plays well with other JavaScript libraries. That means that you can leverage libraries like Prototype or Dojo to allow the JSF UI to interact with your server-side components. The flexibility to use third-party JavaScript libraries in a JSF UI has been a huge challenge for JSF developers in the past, so the Seam JavaScript object is a welcome addition.

The functionality of the Seam Remoting library is divided between the Seam.Component and Seam.Remoting namespace objects. The static methods of Seam.Component provide access to remote Seam components, while the static Seam.Remoting methods are used to control the remoting settings and to create custom data types. As you will see later in the example, a custom data type is any non-primitive object that you need to create locally to call a remote method.

Execution details

To execute a remote method, you first have to obtain an instance of the component that hosts the method. You can use Seam.Component.getInstance() if you want to use an existing Seam component instance (meaning it exists on the server) or Seam.Component.newInstance() if you want to instantiate a new instance before making the call. These instances are actually remote stubs that are built dynamically by Seam. The stub proxies all method calls, marshaling the component name, method, and arguments into XML, and passes the XML payload over the wire to the server-side component using an Ajax request. The remoting framework code on the server picks up this request, unmarshals the XML, extracts the component name, method, and arguments, then invokes the method on the server-side component instance. Finally, the server sends the return value back to the client in the response of the Ajax call. Again, all this work is hidden behind the stub, greatly simplifying your JavaScript code.

That's enough background talk for now. Listing 4 shows how you would execute the drawReason() method on the session-scoped reasonPot component, which was shown flagged as a remote service back in Listing 1. Once the component stub is obtained, executing the method looks like any other JavaScript method call ... or does it?


Listing 4. Invoking a remote method
                <script type="text/javascript">
Seam.Component.getInstance("reasonPot").drawReason(displayReason);

function displayReason(reason) {
  alert(reason);
}
</script>

In Listing 4, you should notice that there is an important difference between the method on the stub and the "real" method on the server-side component. Can you tell what it is? Consider that every call made on a component stub is done asynchronously. That means the result of the remote method call is not immediately available to the executing thread. As such, the method on the stub does not have a value to return, regardless of whether the matching server-side method has one. When a method on the remote stub is executed, it is effectively saying, "Umm, yeah, I'll get back to you on that. Do you have a number where I can reach you?" The number you provide is a "callback" JavaScript function. Its job is to capture the actual return value.

The callback function is a standard construct in asynchronous APIs. It is executed by the Seam Remoting JavaScript framework when a response from an Ajax request arrives back at the browser. If the method on the server-side component has a non-void return value, that value is passed into the callback function as its only argument. On the other hand, if the method on the server-side component has a void return type, then you can disregard this optional parameter.

Conversation-scoped beans

For most of your remoting needs, you can get by with the APIs described so far. When a request is made to retrieve a server-side component or execute one of its exposed methods, that component must be visible within the scope of the remoting life cycle. Referencing session-scoped beans requires no extra work because they are always available to an HTTP request made within the same browser session.

Conversation-scoped beans are trickier, though, because they are associated with an HTTP request based on the presence of the conversation token. When you're working with conversation-scoped beans, you need to be able to establish the correct conversation context during the remote call. The conversation is reactivated by sending the conversation token along with the remote request. The details of this interaction are handled by the Seam Remoting framework, so long as you provide the conversation token to send.

Keep in mind that it is entirely possible for Ajax requests made from the same window to communicate with two or more distinct conversations on the server. Seam can differentiate between them if you specify the conversation ID before retrieving the component instance stub. You accomplish this by using Seam.Remoting.getContext().setConversationId("#{conversation.id} ").

Seam always exposes the current conversation ID under the #{conversation.id} value-binding expression. JavaScript actually sees the resolved value, typically a number, rather than the expression placeholder. You are effectively registering this value with the remoting context, which is then propagated along with the requests that follow. Conversely, you can read the conversation ID assigned during the previous request by calling Seam.Remoting.getContext().getConversationId() after a remote call has been made. This way, your remoting calls can leverage the benefits of the conversation scope and participate in stateful behavior.


Google mapping the Open 18 application

The Reason Pot example in Listing 1 ("Ten good reasons to use Seam") was fun, but now it's time to make the Seam Remoting library do some real work! Shift your focus back to the Open 18 application introduced in "Seamless JSF, Part 2: Conversations with Seam." As you probably recall, Open 18 is a golf course directory. Users can browse the list of courses and then drill down to view the details of a single course. Open 18 also allows the user to create, update, and delete courses. In its original Web 1.0 incarnation, every interaction the user had with this application led to a page being reloaded.

There are various ways to enhance the Open 18 application with the magic of Ajax, and you'll have the opportunity to try a few of them before we're done. The first thing you can do is to create a mashup between Open 18 and Google Maps. You can't travel far on the Internet these days without running into a mapping implementation, and your users will certainly appreciate the addition of a mapping feature in Open 18. Combining the Seam Remoting API and the Google Maps Geocoder API allows you to plot the location of each course in your coarse directory on a Google map, which appears just below the directory listing.

Using the Google Maps API

To use the Google Maps API, you must sign up for an API key, which is completely free. Google only requires the key to maintain statistics on the use of the API and curtail abuse of the service so that it can remain free. You do need a Google account to request a key, however, and according to the terms of service, the key cannot be shared. To comply with those terms, I use a fake key in the example. To run the sample application on your own computer, you need to replace the string GOOGLE_KEY with your own key.

Borrow Google's world view

Geo-spatial mapping might sound tricky, but not when the Google Maps JavaScript API does so much of the work for you. The GMap2 class draws the map and responds appropriately to scrolling and zooming events in the viewport. Another Google Maps class, GClientGeocoder, resolves geo-spatial coordinates based on an address string. Your job is simply to initialize the map and add the markers for each course.

To place the markers onto the map, you first fetch the collection of courses from the server-side component through a remote method call. Next, you translate the address of each course into a geo-spatial point (longitude and latitude) using the GClientGeocoder. Finally, you use that point to place a marker on the map at the corresponding coordinates. As an extra little feature, you can outfit each row in the directory listing with a compass icon that sits next to the edit icon. When the compass icon is clicked in one of the directory rows, the map pans and zooms until the selected course comes into view. At the same time, the map also renders a balloon above the marker that displays the name, address, phone number, and Web site of a given course. This same balloon can be brought up by clicking one of the markers on the map directly. Figure 2 is a preview of what the application will look like when it is all done:


Figure 2. A screenshot of the Google Maps mashup
A screenshot of  the Google Maps mashup.

While the integration of a map component with location-oriented data is interesting in general, this mashup is especially revealing. Users actually are able to view the boundaries of each course on the map! In Map mode, the golf course property is rendered in light green. If the zoom is great enough, a label overlay appears on the course area revealing the name of the course. Even more interesting is Satellite mode, which reveals the course landscape. With enough zoom, it is possible to make out the individual features of each hole, such as the tee box, fairway, and green! This mashup of golf course locations and an interactive map view offers quite a reward for your effort!


Weaving in Google Maps

Google Maps is very easy to integrate and embed into a Web application. As I mentioned already, it takes care of all the details of rendering the map and dually provides an API for plotting geo-spatial locations on that map. The GClientGeocoder object does the tough work of resolving the geo-spatial location and handing back the latitude and longitude data you need.

The method stub that translates addresses to geo-spatial points works similarly to the Seam Remoting method stubs. A callback function is passed to the method when it is invoked to capture the return value. At that time, an Ajax request is sent off to Google HQ and the callback function is executed when the response arrives back at the browser. The intelligent interface of the Google Maps API makes it possible to plot locations very liberally based merely on a postal address. It's a good thing that you do not have to worry about maintaining the geo-spatial coordinates for each course because that would be a real chore! Additional routines on this API then use the latitude and longitude data to construct and render markers for these locations on the map.

Customize it!

A tremendous number of options are available to configure the display of a Google map. Configuring the map itself is not the main focus of this article, but you are encouraged to try it out for yourself. See Resources for a great reference. You may also object to the use of top-level JavaScript functions, rather than using an object-oriented structure to encapsulate the logic. Again, feel free to customize this code to your liking.

Two API methods are relevant for the map integration: Geocoder.getLatLng() and GMap.addOverlay(). First, the Geocoder.getLatLng() method resolves an address string to a GLatLng point. This data object merely wraps a pair of latitude and longitude values. The second argument to this method is a callback JavaScript function that executes once the communication with Google HQ is complete. This function proceeds to add a marker overlay onto the map using GMap.addOverlay(). By default, a marker is rendered as a red pin. The tip of the pin points to the address location on the map.

Listing 5 shows the JavaScript code for setting up your Google map and adding markers to it. The functions are listed in order of execution. You should recognize the Seam Remoting scripts that were used in Listing 3, in addition to the new import of the Google Maps API script.


Listing 5. Mapping Open 18 with Geocoder
                <script type="text/javascript"
    src="http://maps.google.com/maps?file=api&amp;v=2.x&amp;key=GOOGLE_KEY"></script>
<script type="text/javascript"
    src="seam/resource/remoting/resource/remote.js"></script>
<script type="text/javascript"
    src="seam/resource/remoting/interface.js?courseAction"></script>
<script type="text/javascript">
// <![CDATA[
var gmap = null;
var geocoder = null;
var markers = {};
var mapIsInitialized = false;

GEvent.addDomListener(window, 'load', initializeMap);

/**
 * Create a new GMap2 Google map and add markers (pins) for each of the
 * courses.
 */
function initializeMap() {
    if (!GBrowserIsCompatible()) return;
    gmap = new GMap2(document.getElementById('map'));
    gmap.addControl(new GLargeMapControl());
    gmap.addControl(new GMapTypeControl());
    // center on the U.S. (Lebanon, Kansas)
    gmap.setCenter(new GLatLng(38.2, -95), 4);
    geocoder = new GClientGeocoder();
    GEvent.addDomListener(window, 'unload', GUnload);
    addCourseMarkers();
}

/**
 * Retrieve the collection of courses from the server and add corresponding
 * markers to the map.
 */
function addCourseMarkers() {
    function onResult(courses) {
        for (var i = 0, len = courses.length; i < len; i++) {
            addCourseMarker(courses[i]);
        }

        mapIsInitialized = true;
    }

    Seam.Remoting.getContext().setConversationId("#{conversation.id}");
    Seam.Component.getInstance("courseAction").getCourses(onResult);
}

/**
 * Resolve the coordinates of the course to a GLatLng point and adds a marker
 * at that location.
 */
function addCourseMarker(course) {
    var address = course.getAddress();
    var addressAsString = [
        address.getStreet(),
        address.getCity(),
        address.getState(),
        address.getPostalCode()
    ].join(" ");
    geocoder.getLatLng(addressAsString, function(latlng) {
        createAndPlaceMarker(course, latlng);
    });
}

/**
 * Instantiate a new GMarker, add it to the map as an overlay, and register
 * events.
 */
function createAndPlaceMarker(course, latlng) {
    // skip adding marker if no address is found
    if (!latlng) return;
    var marker = new GMarker(latlng);
    // hide the course directly on the marker
    marker.courseBean = course;
    markers[course.getId()] = marker;
    gmap.addOverlay(marker);

    function showDetailBalloon() {
        showCourseInfoBalloon(this);
    }

    GEvent.addListener(marker, 'click', showDetailBalloon);
}

/**
 * Display the details of the course in a balloon caption for the specified
 * marker.  You should definitely escape the data to prevent XSS!
 */
function showCourseInfoBalloon(marker) {
    var course = marker.courseBean;
    var address = course.getAddress();
    var content = '<strong>' + course.getName() + '</strong>';
    content += '<br />';
    content += address.getStreet();
    content += '<br />';
    content += address.getCity() + ', ' +
        address.getState() + ' ' +
        address.getPostalCode();
    content += '<br />';
    content += course.getPhoneNumber();
    if (course.getUri() != null) {
    content += '<br />';
    content += '<a href="' + course.getUri() + '" target="_blank">' +
        course.getUri().replace('http://', '') + '</a></div>';
    }

    marker.openInfoWindowHtml(content);
}

// ]]>
</script>

While Listing 5 appears a bit daunting at first, there really isn't much to it. When the page loads, the Google map is instantiated using the GMap2 constructor, inserted into the target DOM element, and centered on the United States. Et Voila! -- you have a Google Maps display. Easier than you thought, right? Once the map is initialized, the course markers are added. The crux of the code is creating those markers, so let's focus on the addCourseMarkers() function, where the Seam Remoting API is tapped.

Pinpointing the course

To save on server resources, you want your remote call to retrieve the same list of courses that are loaded into the conversation scope while the page is being rendered by JSF. To ensure that the conversation that holds this collection is reactivated during the remote call, the conversation ID must be established on the remoting context, as discussed earlier. To review, the value binding expression that references the current conversation ID, #{conversation.id}, is resolved when the page renders, and its value is then passed to the Remoting Context via the setConversationId() method. Any subsequent calls to a remote method through a component stub will pass along this value and activate the associated conversation. With the conversation active, the same list of courses are available.

Activity indicator

When you run the example, you should notice that Seam places a "Loading..." message in the upper-right corner of the page while the call is on the wire. This message is a considerate way of keeping the user informed that activity is happening in the background. If you would prefer not to show this message or if you would like to customize it, you can find methods for doing so on the Seam.Remoting JavaScript object.

To pull down the courses, the next step is to grab a reference to the component named courseAction, using Seam.Remoting.getInstance(), and then execute the getCourses() method on that stub. As explained earlier, the stub method takes a callback function as its final argument, which is used to capture the list of courses in the response. Notice that the return value is no longer a primitive type but rather a collection of user-defined JavaBeans. Seam creates JavaScript structures to emulate any JavaBean classes that are used in the method signatures of the server-side components. In this case, Seam unmarshals the response to JavaScript objects that represent Course entities, with all the same getters and setters. The getter methods on the course object are used in various places in Listing 5 to read the course data.

In the final step of placing the courses on the map, the address for each course is converted into a GLatLng point using the GClientGeocoder. That value is then used to create a GMarker widget that is added as an overlay on the map.

A map without a compass

Now your map is decorated with all these fancy markers, but there is no way to associate the rows in the course directory with the markers on the map. This is where the compass icon comes into play. You are going to add a little bit more JavaScript that uses the Google Maps API to zoom and center the map on the course when the compass icon for that course is clicked. Listing 6 shows the component tag for the compass icon. This component is inserted in each row in the directory, just before the edit icon. (The functionality of editing a course is described in "Seamless JSF, Part 2: Conversations with Seam.")


Listing 6. The component tag that renders the compass icon
                <h:graphicImage value="/images/compass.png" alt="[ Zoom ]" title="Zoom to course on map"
  onclick="focusMarker(#{_course.id}, true);" />

The JavaScript event handler, focusMarker(), is registered to handle the onclick event on the compass icon. The focusMarker() method, shown in Listing 7, looks up the GMarker previously registered for that course in a global registry and then delegates the work to the showCourseInfoBalloon() function from Listing 5:


Listing 7. The focusMarker() event handler focuses the map on the selected course
                /**
 * Bring the marker for the given course into view and display the
 * details in a balloon.  This method is registered in an onclick
 * handler on the compass icons in each row in the course directory.
 */
function focusMarker(courseId, zoom) {
    if (!GBrowserIsCompatible()) return;
    if (!mapIsInitialized) {
        alert("The map is still being initialized. Please wait a moment and try again.");
        return;
    }
    var marker = markers[courseId];
    if (!marker) {
        alert("There is no corresponding marker for the course selected.");
        return;
    }

    showCourseInfoBalloon(marker);
    if (zoom) {
        gmap.setZoom(13);
    }
}

Restore the course collection

Listing 8 reveals the method on the server-side component that is responsible for exposing the previously fetched courses. (The CRUD action handlers created for the Open 18 implementation in "Seamless JSF, Part 2: Conversations with Seam" are excluded from this listing for brevity.)


Listing 8. The CourseAction component exposes the getCourses method
                
@Name("courseAction")
@Scope(ScopeType.CONVERSATION)
public class CourseAction implements Serializable {
    /**
     * During a remote call, the FacesContext is <code>null</code>.
     * Therefore, you cannot resolve this Spring bean using the
     * delegating variable resolver. Hence, the required flag tells
     * Seam not to complain.
     */
    @In(value="#{courseManager}", required=false)
    private GenericManager<Course, Long> courseManager;

    @DataModel
    private List<Course> courses;

    @DataModelSelection
    @In(value="course", required=false)
    @Out(value="course", required=false)
    private Course selectedCourse;

    @WebRemote
    public List<Course> getCourses() {
        return courses;
    }

    @Begin(join=true)
    @Factory("courses")
    public void findCourses() {
        System.out.println("Retrieving courses...");
        courses = courseManager.getAll();
    }

    // .. additional CRUD action handlers ..
}

As explained earlier, the method stub can include an optional callback JavaScript function as its last argument. That is why the getCourses() object on the client-side stub takes a single argument, while the corresponding method on the server-side component takes none. The getCourses() method returns the collection of courses present at the time when the page is rendered.


Life without the FacesContext

By now, you may be wondering how all these Seam Remoting requests play in with the JSF life cycle. Well, the fact is they don't. At least not in the typical way. Not awakening the JSF life cycle is what keeps the requests so lightweight. When a method is invoked on a component stub from the JavaScript, the call is channeled through the Seam Resource Servlet and handled by the Remoting delegate object. Because the Resource Servlet does not take requests through the JSF life cycle, the FacesContext is not available during the remote call. Instead, the Resource Servlet uses its own life cycle that merely taps into the Seam component container. Bottom line: Any value-binding expression that does not resolve to an object managed by Seam will be null when a method is executed through the Seam Remoting API. Additionally, JSF component bindings used by the backing bean will not be available either.

In the initial configuration of the Open 18 application, the CourseAction bean relies on the Spring framework's JSF variable resolver to inject a CourseManager object into the backing bean through the value-binding expression #{courseManager}. The trouble is, the Spring variable resolver relies on the FacesContext to locate the Spring container. If the FacesContext isn't available, then the variable resolver can't access any of the Spring beans. Thus, the value-binding expression #{courseManager} will be null when the component is accessed via the Seam Remoting API.

This situation presents a challenge if you need to use the CourseManager object for anything during a remote method call. Because the default behavior of Seam is to enforce the existence of dependencies, you need to mark the @In annotation with the attribute required=false, which indicates that it is optional. Doing this keeps Seam from complaining when a call is made through a remote stub.

The next sections shows how to use Ajax4jsf to merge your application's course directory page and course detail page. This enhancement does require the service layer object to persist the changes to a course. However, because Ajax4jsf uses the complete life cycle when it invokes action handlers, the #{courseManager} expression resolves in the normal way.


Re-rendering with Ajax4jsf

The Google Maps mashup is very exciting, but adding it to the Open 18 application introduces a small design problem. When the user clicks on a course name in the directory, to view detailed information about that course, the page refreshes. As a result, the map is frequently reloaded. These reloads add way too much overhead to both the browser and the server and restore the map to its initial position.

The Ajax4jsf project

The Ajax4jsf project was initially started by Exadel and hosted at Java.net. Recently, JBoss has absorbed the Ajax4jsf library as part of its acquisition of Exadel's products. When Ajax4jsf is combined with JSF, the vision of an event-driven interface is truly realized. Events can trigger actions on the server without incurring the cost of a page reload, yet the UI is still updated to reflect any changes in the page structure. Ajax4jsf has proved to be a natural fit for Seam's objective to provide a "rich" Web platform built on JSF.

To prevent the map from reloading, you must restrict all activity to a single page load. Unfortunately, the process of submitting a form is tied to a page request. This tight coupling is default behavior for Web browsers. What you need is for the data to be sent to the server without the browser requesting the page again -- which is precisely the challenge the Ajax4jsf component library was designed to resolve. Swapping out the h:commandLink tag with the a4j:commandLink tag in the Name column of the directory listing enables Ajax4jsf to work its magic whenever a course link is clicked. When the Ajax4jsf version of the link is activated, it executes the method indicated by the method-binding expression in the action attribute, #{courseAction.selectCourseNoNav}, and then returns replacement markup containing the course details to be inserted below the directory.

The selectCourseNoNav() method performs the same logic as the selectCourse() method but it has no return value to ensure that JSF will not pursue a navigation event. After all, the whole point of the Ajax request is to prevent the browser from requesting the page again. The XHTML markup returned is inserted in the area below the map without disrupting the map's state. The component shown in Listing 9 replaces the h:commandLink used in the original version of courses.jspx, found in "Seamless JSF, Part 2: Conversations with Seam":


Listing 9. The Ajax4jsf-managed link for selecting a course
                
<a4j:commandLink id="selectCourse"
  action="#{courseAction.selectCourseNoNav}" value="#{_course.name}" />

As mentioned earlier, there are two ways to indicate which areas of the page to incrementally update. One way is to target individual components by specifying their component IDs in the reRender attribute of the Ajax4jsf action component. The other way is to wrap regions of the page in an a4j:outputPanel tag and mark those regions as "Ajax rendered" using the ajaxRendered attribute on that tag. Listing 10 shows the region of the view template that outputs the details of a selected course using the output-panel approach:


Listing 10. The Ajax-rendered course details panel
                
<a4j:outputPanel id="detailPanel" ajaxRendered="true">
  <h:panelGroup id="detail" rendered="#{course.id gt 0}">
  <h3>Course Detail</h3>
  <!-- table excluded for brevity -->
  </h:panelGroup>
</a4j:outputPanel>

Configuring Ajax4jsf

Ajax4jsf's custom view handler

The view-handler delegation mechanism in the JSF specification is fairly limited and does not deal with view handler chains very well. Ajax4jsf uses a custom view handler to decorate the actual view handler as a workaround.

Before you can run the updated code, you need to configure Ajax4jsf in your application. First, add the jboss-ajax4jsf.jar library and its dependency, oscache.jar, to your application's classpath. Second, add the Ajax4jsf filter to your web.xml. The definition for this filter is shown in Listing 11. Note that the Ajax4jsf filter must be the first filter defined in the web.xml file. Much like Seam's Resource Servlet, this filter handles remote calls initiated by Ajax4jsf components. Unlike Seam's remote calls, however, the JSF life cycle is active during an asynchronous Ajax4jsf request, hence the need for a servlet filter rather than a regular servlet. You also need to shift the configuration of the Facelets view handler from the faces-config.xml file to the web.xml, where it is defined in a servlet context parameter.


Listing 11. Configuring Ajax4jsf
                <context-param>
    <param-name>org.ajax4jsf.VIEW_HANDLERS</param-name>
    <param-value>org.jboss.seam.ui.facelet.SeamFaceletViewHandler</param-value>
</context-param>

<filter>
    <filter-name>Ajax4jsf Filter</filter-name>
    <filter-class>org.ajax4jsf.Filter</filter-class>
</filter>

<filter-mapping>
    <filter-name>Ajax4jsf Filter</filter-name>
    <url-pattern>*.action</url-pattern>
</filter-mapping>

Once you have included the libraries and installed the markup in Listing 11 in the web.xml file, you are ready to use Ajax4jsf! Just be sure to add the Ajax4jsf tag library definition, xmlns:a4j="https://ajax4jsf.dev.java.net/ajax", to the top of any view template in which you use the Ajax4jsf tags. Of course, much more is possible with this library than I can cover in one article. See Resources for more information and examples related to using Ajax4jsf.


Holler back!

Up to this point, the Open 18-Google Maps mashup has been entirely read-only. Much of the excitement around Ajax is about being able to change back-end data without reloading the page. I think it's time to have a little fun with our Google map so that it sends some data back. Assume, for the sake of demonstration, that even though Google can resolve any postal address to a latitude and longitude coordinate, the user still wants to be able to specify a custom location for the pin. To be precise, let's say the user wants the pin to point to the clubhouse or the first tee of a given course. Fortunately, the Google Map API makes this final enhancement to the Open 18 application very easy to pin down.

You start by allowing the marker to be dragged. To turn this feature on, simply add the draggable flag to GMarker when it is instantiated. Next, register a JavaScript function that listens for the "dragend" event that is fired when a user finishes dragging the marker. Inside of this callback function, you execute a new method on the courseAction stub, setCoursePoint(), to save this new point.

Listing 12 shows the modified createAndPlaceMarker function. The addCourseMarker() function is also modified to consider the custom point on the Course entity when placing the marker.


Listing 12. Updated JavaScript with marker dragging enabled
                function addCourseMarker(course) {
    var address = course.getAddress();
    if (course.getPoint() != null) {
        var point = course.getPoint();
        var latlng = new GLatLng(point.getLatitude(), point.getLongitude());
        createAndPlaceMarker(course, latlng);
    }
    else {
        var addressAsString = [
            address.getStreet(),
            address.getCity(),
            address.getState(),
            address.getPostalCode()
        ].join(" ");
        geocoder.getLatLng(addressAsString, function(latlng) {
            createAndPlaceMarker(course, latlng);
        });
    }
}

function createAndPlaceMarker(course, latlng) {
    // skip adding marker if no address is found
    if (!latlng) return;
    var marker = new GMarker(latlng, { draggable: true });
    // hide the course directly on the marker
    marker.courseBean = course;
    markers[course.getId()] = marker;
    gmap.addOverlay(marker);

    function showDetailBalloon() {
        showCourseInfoBalloon(this);
    }

    function assignPoint() {
        var point = Seam.Remoting.createType("com.ibm.dw.open18.Point");
        point.setLatitude(this.getPoint().lat());
        point.setLongitude(this.getPoint().lng());
		var courseActionStub = Seam.Component.getInstance("courseAction");
		courseActionStub.setCoursePoint(this.courseBean.getId(), point);
    }

    GEvent.addListener(marker, 'click', showDetailBalloon);
    GEvent.addListener(marker, 'dragstart', closeInfoBalloon);
    GEvent.addListener(marker, 'dragend', assignPoint);
}

You're not done just yet, but you are so close! You still need to add a method to the server-side component that saves this GLatLng point. Bear with me, though, because you need to revisit the Open 18 application's Spring integration before completing this final feature of the mashup. (See "Seamless JSF, Part 2: Conversations with Seam" to revisit the details of the Spring integration.)


Spring integration redux

As discussed earlier, the variable-resolver approach I originally used to integrate the Spring container into Seam has its limitations. Frankly, it has been stretched to the point that it is beginning to tear, and it's time to bid it farewell. While most of Seam's features involve JSF, some aspects of Seam work outside of the JSF life cycle. To achieve true integration with Spring, you need a better solution than a custom variable resolver. Fortunately, Seam's developers have addressed this need by adding a Seam extension for Spring. The Spring integration package leverages a new feature in Spring 2.0 for creating schema-based extension points. These extensions enable the use of custom XML namespaces in the bean definition file and namespace handlers that operate on those tags during the Spring container startup process.

The Seam namespace handler for Spring (you may have to say that a few times aloud before it makes sense) has several ways of interacting with the Spring container. To eliminate the custom variable resolver, you need to somehow expose a Spring bean as a Seam component. The seam:component tag is used for this purpose. Placing this tag inside of a Spring bean declaration notifies Seam that the Spring bean should be proxied, or wrapped, as a Seam component. At this point, the Seam bijection mechanism treats the bean just as if it had been annotated with @Name -- only now, you no longer need the FacesContext to bridge the gap between Seam and Spring!

Configuring the Seam-Spring integration is surprisingly easy. The first step doesn't even involve any code changes! All you need to do is ensure that you are using both Spring 2.0 and Seam 1.2.0, or a subsequent release. (Seam has gone through rapid changes since the first article in this series was published, so you should be about ready for an upgrade anyway.) The IOC integration is packaged as a separate JAR, jboss-seam-ioc.jar, so you need to include it in your application's classpath in addition to the JARs already mentioned.

The second step doesn't involve the Seam configuration either. This time, you look at your Spring configuration. First, add the Seam XML schema declarations to the Spring configuration XML in which your beans are defined. The standard name for this file is applicationContext.xml, though the example application uses a more fitting name, spring-beans.xml. Next, add the seam:component tag inside of the definition of any Spring bean you would like to expose to JSF. In the Open 18 application, you nest this tag inside of the courseManager bean definition. To see the full bean listing, refer to the spring-beans.xml file for this article's example application.

With the improved Spring integration in place, you no longer need to specify a value binding in the @In annotation atop the courseManager property, nor do you need the required=false attribute. In its place, use the create=true attribute so that Seam knows to go fetch the bean from the Spring container and decorate it as a Seam component. The code in Listing 13 shows the relevant portions of the CourseAction class that are needed to support updating the geo-spatial point of the course:


Listing 13. The new setPoint method on the CourseAction
                @WebRemote
public void setCoursePoint(Long id, Point point) {
    System.out.println("Saving new point: " + point + " for course id: " + id);
    Course course = courseManager.get(id);
    course.setPoint(point);
    courseManager.save(course);
}

Now put on your shades and act like Joe Cool. That's right: You have officially "Ajaxified" your application!


Conclusion

In this article, you saw the Open 18 application expand from a fairly simple CRUD implementation to a far more sophisticated mashup integrating Ajax-style page rendering and Google Maps. You've also seen what you can do with the Seam Remoting API and Ajax4jsf and learned a more sophisticated way to integrate Seam with the Spring framework.

Both Seam Remoting and Ajax4jsf complement JSF's component model by extending its abstraction to include Ajax communication. Nowhere in any of the code presented in this article is it necessary to interact directly with the XMLHttpRequest JavaScript object. Instead, all the work is hidden behind higher level APIs that let you either query and manipulate server-side components directly, or invoke actions and update regions of a given page. And, thanks to the magic of Ajax, all that work happens asynchronously.

Over the course of the three articles in this series, you've learned a tremendous amount about JSF development with Seam, but I've by no means exhausted all of Seam's features! Let the discussion and examples in Seamless JSF serve as a starting point for learning even more about what Seam can do to make your JSF projects a pleasure to create. The more you work with Seam, the more you'll find to love about it.



Download

DescriptionNameSizeDownload method
Open 18 sample application - Phase 21j-seam3.zip277K HTTP

Information about download methods

Note

  1. The sample application is organized as a Maven 2 project. All dependencies will be fetched on demand when the build is executed. The application uses several libraries from the Appfuse project to implement the service and DAO layers.

Resources

Learn

Get products and technologies

  • JBoss Seam: Download it to get the complete distribution, including the bundled example applications.

  • Ajax4jsf: Adds Ajax functionality to existing JSF components.

  • RichFaces: Builds on Ajax4jsf to add out-of-the-box "richness" for JSF Web applications.

  • Firebug: This plug-in for Firefox is an indispensable tool when working with Ajax applications.

  • Maven 2: A software project management and comprehension tool used in the source code samples. Maven automatically downloads dependencies during the build process.

  • Facelets: The preferred JSF view handler for Seam applications.

  • Appfuse: Practically hands you a remarkably comprehensive Maven 2-based project skeleton on which to build your application.

Discuss

About the author

Dan Allen

Dan Allen is currently a senior Java engineer at CodeRyte, Inc. He is also a passionate open source advocate and gets a bit manic whenever he catches sight of a penguin. After graduating from Cornell University with a degree in Materials Science and Engineering, Dan became captivated by the world of Linux and open source software. He has been slaving over Web applications ever since, with the last several years focused exclusively on Java-related technologies, including Spring, Hibernate, Maven 2, and the abundant JSF stack. You can keep up with Dan's development experiences by subscribing to his blog at http://www.mojavelinux.com.

Comments (Undergoing maintenance)



Trademarks  |  My developerWorks terms and conditions

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology, Web development, Open source
ArticleID=230022
ArticleTitle=Seamless JSF, Part 3: Ajax for JSF
publish-date=06122007
author1-email=dan.allen@mojavelinux.com
author1-email-cc=

My developerWorks community

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere).

My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Rate a product. Write a review.

Special offers