To ease the workload of non-IT professionals, including business consultants, we are building a prototype modeling tool, with Microsoft® PowerPoint–like presentation features, that helps consultants organize large amounts of unstructured data and prepare presentation documents for customers. The first version of our tool was built on Eclipse. However, to take advantage of better graphics, we decided to switch to Rich Internet Application technologies. We considered the pros of cons of two popular platforms—Ajax or Adobe Flex—and decided to go with Flex.
Our Flex-based application allows users to construct slides using a variety of visualization assistants, including bulleted lists, graphic editors, tag clouds, relation explorers, and tables. In this article, we'll explain one particular assistant that allows users to arrange, classify, and compare potentially large quantities of data. We call such assistant a "Juxtaposition Table" or JTable. With JTable, users can visualize data in two dimensions and interactively change the horizontal and vertical data sets at runtime. This supports the examination of a database or information space from a variety of user-defined perspectives in a single view.
Before looking at the code, let us introduce our basic scenario, in which we, as an example, are a survey center, specializing in public tastes of favorite books, movies, songs, and sites. A polling group can have any number of participants as well as an expandable list of polling categories. For simplicity’s sake, we kept our polling group small. The participants were: John, Jennifer, and Ivan. After completing the survey, we transferred the results into a simple database, and now we want to make the results available on our Web site for general perusal. We could provide one large html table which contains all the data from our survey, but this may be rather cumbersome for users due to the table's size and the information density. Alternatively, we could provide a number of tables which present the data from a variety of predefined perspectives. However, this still is not ideal as users are restricted to our predefined table plots, and scrolling between the tables may become bothersome.
An elegant solution to our problem is a JTable. If we present our data in the JTable, users can choose a custom perspective to suit their informational needs by changing the horizontal and vertical data sets. For instance, a user might wish to see John’s favorite books; this may be achieved by selecting "John" as the horizontal dimension in the table and "Favorite Books" as the vertical dimension. The user might then become interested in examining the survey participants' movie preferences. Again this is achieved by selecting "All People" in the horizontal dimension and "Favorite Movies" in the vertical dimension. Using JTable, the user is free to define his or her own custom perspectives in a convenient and compact view. Run the JTable application by downloading DataGridDeveloperWorksExample1 in the Download table below. After that, you can view the survey data in different combinations.
We use Flex’s AdvancedDataGrid
component to implement the JTable. In this two-part series, we will demonstrate
how we customized and extended the AdvancedDataGrid
component provided in the Flex 3 data visualization package. You can read more
about it in the Resources section of this article. In
this article we will also demonstrate how we used horizontal and vertical control
bars to alter the JTable content at runtime and how we made the first column
and the first row of the table into row and column headers. In a subsequent
article we will demonstrate the fluid expansion and contraction of items in
cells and the capability of moving items from cell to cell by using
drag-and-drop.
These articles assume readers are familiar with the Flex 3 programming environment and the ActionScript language.
Comparing a mockup table with the JTable from the running example
Before going to the implementation solutions for the JTable application, let us first examine a mockup of one of a possible perspectives of the data we collected from the survey and then give you an overview of our JTable.
What we wanted to achieve was a table with a horizontal header presenting a single name of a person, or names of all people, and a vertical header presenting a single category, or all categories of favorite items. Table 1 is a mockup of how we would like our table to look after a user selects "All People" for the horizontal dimension and "All questions" for the vertical dimension.
Table 1. 2D mockup table displaying favorite items associated with a polling group
| John | Jennifer | Ivan | |
|---|---|---|---|
| Favorite book | Book - 1 | Book - 2 | Book - 3 |
| Favorite movie | Movie - 1 | Movie - 2 | Movie - 3 |
| Favorite song | Song - 1 | Song - 2 | Song - 3 |
| Favorite site | Site - 1 | Site - 2 | Site - 3 |
Figure 1 shows the concept realized with JTable.
Figure 1. 2D JTable displaying favorites associated with the polling group
What you see in Figure 1 is a 2-dimensional lookup of our data. You can see that the header labels that are usually present in tables do not show up in this one (though the header is slightly visible at the very top of the table, as a narrow band with vertical table lines that allow the user to change the width of the columns). You should also observe that the table has identical looking and equally serviceable horizontal and vertical headers that are distinguished from the table's body. Further in the article we will use two terms: a header cell and a content cell. We define a header cell as a cell for the top and side headers that we call horizontal and vertical headers; we define a content cell as a cell that is not in a header but within the body of the table. If you take another look at Figure 1, you'll see that each header cell contains only one item encapsulated in an oval, while each content cell contains a few items, with each of the items displayed against a darker background. One more look at the figure will show two menu buttons, one above the horizontal header and a second to the left of the vertical header. The pull-down menus of the two buttons allow a user to dynamically configure the content of the headers and, consequently, the table.
Before we go on to our implementation solutions for the JTable application, we'd like to show you how we generated the data to run the JTable application.
In a real scenario, survey data would most likely be
retrieved from a database on a server. However, for the purpose of this
example we used a simple DataBase class and
populated it with answers given by members of the survey group. The questions
themselves are stored as categories. We made our database a singleton in order
to limit the DataBase class to only one instance.
The following line of code gives access to that instance:
var fRowSet = DataBase.instance.getQuestions();
|
We store the answers of all participants in a repository, associating each participant’s answers with his or her name. The names of the respondents serve as keys to query the repository. We store the answers to each question in a separate repository with keys that coincide with questions, such as "Favorite Book," "Favorite Movie," "Favorite Song," and "Favorite Site." After finishing populating a single participant’s repository with answers, we add his or her answers to the all-answer repository (fMap).
Listing 1. Setting up John's answers
var fMap:Hashmap = new Hashmap();
var johnsAnswers:Hashmap = new Hashmap();
var johnsBooks:Array = new Array("The Human Stain", "One More Year","The Painted Bird");
johnsAnswers.put("Favorite Books", johnsBooks);
…
fMap.put("John", johnsAnswers);
|
We
put Jennifer’s and Ivan’s answers in the all-answer repository in a similar
manner. Then, we use three methods in the DataBase
class to retrieve the
data:
Listing 2. Retrieving the data
public function getPeople():Array
public function getQuestions():Array
public function getAnswers(person:String = null, question:String = null):Array
|
The
getPeople method returns an array of all the
survey participants as strings. The getQuestions
method returns an array of all the survey categories as strings. Finally, the
getAnswers method takes two parameters as an
input ( person’s name and a polling category) and returns an array of
answers as strings. Array structures returned from these methods are used to
build our data model. The getAnswers operation can
be invoked with a null person or question parameter.
The rest of the
article describes our design and implementation solutions for the JTable,
including the modifications we needed to make to the AdvancedDataGrid component, as well as other support we created so
that our JTable could work in the way we show in the running
example.
The JTable data provider is built on a foundation
of rows and cells. Because a table is made up of rows, each of which contains
a number of cells, we achieve this mapping between the data provider and the
table by first defining the Row class and the Cell class.
As the names of these classes
suggest, the Row class holds objects for a single
row, and a Cell class, shown in the code below
(Listing 3), is for a content of a cell in a row. Each cell contains a generic
data variable. What we will store in this data variable is an array of
two elements: a horizontal header name and a vertical header name of a
cell.
Listing 3. Cell class
public class Cell
{
private var fData:*;
public function Cell()
{
}
public function get data():*
{
return fData;
}
public function set data(value:*):void
{
fData = value;
}
}
|
We
created a wrapper class, called HeaderCell to
distinguish a header cell form a content cell. We have created another
wrapper, NullCell class, which is used for the
upper left corner cell that's neither a "header" nor a "content" cell.
A row content is defined by its Cell
objects. The Row class, as you see below in Listing
4, is a holder of Cell objects. The Row class is dynamic. Dynamic objects in Flex act as
hashmaps. The Row class is altered at runtime when
cell objects belonging to a row are added to it. Cell objects are being inserted into the Row object via a column index. Thus, the Cell objects are being hashed with the column index in the Row
object.
Listing 4. Row class
public dynamic class Row
{
private var fColumIndex:int = 0;
public function Row()
{
}
public function putCell(cell:Cell):void
{
this[fColumIndex++] = cell;
}
public function getCellAt(index:int):Cell
{
return this[index] as Cell;
}
}
|
Having
looked at Cell and Row
classes, we are now ready to move on to other implementation solutions for the
JTable application. First, we’d like to show how we were able to protect the
JTable’s UI code from being altered by any data changes. We accomplished this
by completely separating the data from the user interface.
We defined
the table's data model in the DefaultDataModel
class. The createDataPovider function in this class
provides content for the table. The function takes two parameters: "array
of rows" and "array of columns". These parameters are passed to the
DefaultDataModel class constructor, which is
shown in the fragment of code below in Listing
5.
Listing 5. Passing parameters to the DefaultDataModel class
private var fRowSet:Array;
private var fColumnSet:Array;
public function DefaultDataModel(rowSet:Array, columnSet:Array)
{
fRowSet = rowSet;
fColumnSet = columnSet;
}
|
Each time the user wants to change the appearance of the table by selecting a menu option, through either of the horizontal or vertical pop-up menu buttons, the data model behind the table is altered as it is shown in the fragment of code in Listing 6.
Listing 6. Changing the data model
public function get dataProvider():ArrayCollection
{
if(!fDataProvider)
fDataProvider = createDataProvider();
return fDataProvider;
}
|
As
you can see from the code fragment in Listing 7, below, the createDataProvider function forks the code into three other
functions: createRowAndColumnDataProvider, createColumnOnlyDataProvider and createRowOnlyDataProvider. createRowAndColumnDataProvider creates a data provider that
provides content for a 2D table. createColumnOnlyDataProvider and createRowAndColumnDataProvider create providers for a 1D table.
A 1D table is displayed when a user selects the "Clear" option, either in the horizontal or vertical menu buttons (Figures 2, 3).
Listing 7. createDataProvider
protected function createDataProvider():ArrayCollection
{
if (fRowSet && fColumnSet)
return createRowAndColumnDataProvider();
if (fColumnSet && !fRowSet)
return createColumnOnlyDataProvider();
if (fRowSet && !fColumnSet)
return createRowOnlyDataProvider();
return new ArrayCollection();
}
|
In
the next fragment (Listing 8) we will show you one of the three "Create" data
provider functions, createRowAndColumnDataProvider.
This function provides the content for a 2D table. First, we instantiate a
header row and add a corner cell to it. Then we loop over columns (first loop)
and add the rest of the header cells to this row and add the row to the
provider after the looping is done. The second loop is done over the rows. For
each element in the fRowSet array, a new row is
instantiated. The first cell added to a row is a horizontal header's cell;
other cells added to a row are content cells.
Listing 8. DefaultDataModel class: createRowAndColumnDataProvider function
protected function createRowAndColumnDataProvider():ArrayCollection
{
var provider:ArrayCollection = new ArrayCollection();
var headerRow:Row = new Row();
// Put in the top corner cell
headerRow.putCell(new NullCell());
// Populate the row header
for each(var item:String in fColumnSet)
{
// Create the header cells
var cell:Cell = new HeaderCell;
cell.data = item;
headerRow.putCell(cell);
}
provider.addItem(headerRow);
// Populate the contents and column header
for each(var rowItem:String in fRowSet)
{
// Content row
var row:Row = new Row();
// Column header cell
var headerCell:Cell = new HeaderCell();
headerCell.data = rowItem;
row.putCell(headerCell);
for each(var columnItem:String in fColumnSet)
{
// Create the content cells
var cell:Cell = new Cell();
cell.data = new Array(rowItem,columnItem);
row.putCell(cell);
}
provider.addItem(row);
}
return provider;
}
|
The
array collection returned from the createRowAndColumnDataProvider function will be assigned to the
table through its dataProvider property.
We built another data model to
provide content for horizontal and vertical menu buttons that control the
content of the headers and the table. We defined this model in the PopUpButtonsDataModel class. This class has only two
functions: getRowMenu and getColumnMenu. You can see one of these functions in the code
fragment below (Listing 9). In this fragment we create an array collection of
menu items. First we add "Clear" and "All Questions" menu items to the menu
array collection, and then we add the survey categories in a simple loop. The data
we store in each menu item object is an array containing only one element: the
name of the category. The array collections of MenuItem objects returned from getRowMenu and getColumnMenu will be
assigned to the menu buttons through their dataProvider
property.
Listing 9. PopUpButtonsDataModel class: getRowMenu function
public function getRowMenu(): ArrayCollection
{
var menu:ArrayCollection = new ArrayCollection();
var questions:Array = DataBase.instance.getQuestions();
var menuItem:MenuItem = new MenuItem("Clear");
menu.addItem(menuItem);
var menuItem:MenuItem = new MenuItem("All questions");
menuItem.data = questions;
menu.addItem(menuItem);
for each(var question:String in questions)
{
var menuItem:MenuItem = new MenuItem(question);
menuItem.data = new Array(question);
menu.addItem(menuItem);
}
return menu;
}
|
The UI visible in Figures 1, 2, and 3 is a canvas container
that holds three user interface components: the table and two menu buttons.
Our canvas class, DynamicAdvancedDataGridCanvas,
extends Flex's Canvas class. We add the table and
two menu buttons to the canvas container in the createChildren function (Listing 11), which overrides the same
function in the base class. We used the menu buttons to allow a user to
examine the survey by selecting options from the buttons' pull-down menus.
(The buttons here are also known as the horizontal and vertical control bars.)
We created the vertical control bar by rotating the row button 90 degrees
counterclockwise (-90), as shown in Listing 11 below. However, this meant that
an embedded font was required, as a standard font does not show up in the
vertical button. Listing 10 shows the code we used to include an embedded font
declaration in the application DataGridDeveloperExample1.mxml
file.
Listing 10. Code to embed font in DataGridDeveloperExample1.mxml
[Embed(systemFont='Verdana', fontWeight="bold", fontName='embeddedFont', mimeType='application/x-font', advancedAntiAliasing="true")] |
Listing 11. DynamicAdvancedDataGridCanvas class: createChildren function
override protected function createChildren():void
{
super.createChildren();
fColPopupMenuButton = new SuperPopUpMenuButton();
...
PopupMenuButton.addEventListener(MenuEvent.ITEM_CLICK, columnClick);
fColPopupMenuButton.addEventListener(DropdownEvent.OPEN, colPopupOpen);
fColPopupMenuButton.dataProvider =
fPopUpButtonsDataModel.getColumnMenu().toArray();
addChild(fColPopupMenuButton);
fRowPopupMenuButton = new SuperPopUpMenuButton();
...
fRowPopupMenuButton.rotation = -90;
...
fRowPopupMenuButton.addEventListener(MenuEvent.ITEM_CLICK, rowClick);
fRowPopupMenuButton.addEventListener(DropdownEvent.OPEN, rowPopupOpen);
...
fRowPopupMenuButton.dataProvider = fPopUpButtonsDataModel.getRowMenu().toArray();
addChild(fRowPopupMenuButton);
fAdvancedDataGrid = new DynamicAdvancedDataGrid();
addChild(fAdvancedDataGrid);
...
}
|
The
menu buttons, which we added to the canvas, are event-driven, meaning our
canvas class handles events that are generated when the user opens and selects
an option within the drop-down menu (DropdownEvent.OPEN and MenuEvent.ITEM_CLICK events).
Figure 2. 1D table displaying all favorites of the polling group, with vertical menu open
Figure 3. 1D table displaying favorites of the polling group with the horizontal menu open.
In the next fragment (Listing 12) we show how we handle the DropdownEvent. The data provider is assigned through
the menu button's dataProvider property. It is
worth noticing how the y coordinate of the popUp
window of the vertical menu button is converted from local to global
coordinates in the last line of this code fragment. We do not need to do this
conversion for the horizontal menu
button.
Listing 12. DropdownEvent handling
public function rowPopupOpen(event:DropdownEvent):void
{
if(fRowMenuInvalid)
{
fRowPopupMenuButton.dataProvider =
fPopUpButtonsDataModel.getRowMenu().toArray();
}
fRowPopupMenuButton.popUp.y =
localToGlobal(new Point(0,fRowPopupMenuButton.y)).y
- fRowPopupMenuButton.width;
}
|
Below,
in Listing 13, we show how we handle the second event, the MenuEvent. The first line changes the button's label to show the
selected label in the pull-down menu. The second line retrieves the menu data
object from the event. The last line in this function refreshes the
table.
Listing 13. MenuEvent handling
private function rowClick(event:MenuEvent):void
{
fRowPopupMenuButton.label = event.label;
fRowSet = (event.item as MenuItem).data;
refresh();
}
|
Listing
14 shows the refresh
function.
Listing 14. Refresh function
public function refresh():void
{
var model:DefaultDataModel = new DefaultDataModel(fRowSet,fColumnSet);
fAdvancedDataGrid.dataProvider = model.dataProvider;
}
|
Now
let's explore the creation of the table itself in greater detail. We define
the table with the DynamicAdvancedDataGrid class,
which extends Flex's AdvancedDataGrid
class.
The Flex AdvancedDataGrid is a UI
component, contained in the Flex data visualization package, which is capable
of displaying multiple columns of information. Each column in the AdvancedDataGrid component is represented by an AdvancedDataGridColumn object. The headerText and dataField properties are
the two main properties that define an individual column. The headerText is the name that appears as the title of a
column, while the dataField indicates that the data
from the data provider is to be displayed in the field. The data, which is a
collection of objects, is assigned to the AdvancedDataGrid component through the dataProvider property.
The simplest case for creating an
application with the AdvancedDataGrid component is
shown in the fragment of code in Listing 15, below. In the fragment, we bind
the variable dataModel to the dataProvider
property.
Listing 15. Creating a simple application with AdvancedDataGrid
private var dataModel:ArrayCollection =
new ArrayCollection([
{Person :"John", Favorite Book: "The Human Stain"},
{Person :"Jennifer", Favorite Book: "One More Year"},
{Person :"Ivan", Favorite Book: "The Painted Bird"}]);
fAdvancedDataGrid = new AdvancedDataGrid();
fAdvancedDataGrid.dataProvider = dataModel;
fAdvancedDataGrid.percentWidth = 100;
fAdvancedDataGrid.percentHeight = 100;
addChild(fAdvancedDataGrid);
|
Run this simple example by downloading DeveloperWorksDataGridBaseExample
in the Download table below.
In
this example, two columns—Person and Favorite Book—are displayed in the table based on the format and content of the
data provider.
Columns automatically pick up the "keys," Person
and Favorite Book,
from the data provider to determine what fields in that same data provider to
use when displaying information. While these few lines of code are enough to
build an application with a default AdvancedDataGrid, the component has limitations when it comes to
the implementation of our JTable. We found that the three major limitations
were: the raw format of the data provider, the fixed number of columns, and
the lack of support for color coding of cells. Raw data providers are not
sufficient for data that is always changing, because the components do not
receive notification of data changes. Our solution for JTable was to refresh
our table by reassigning the data provider each time a user selected a new
option from one of the button's pull-down menus. We overcame the limitation of
a fixed number of columns by rebuilding columns dynamically when a change in
data occurred. As you will see below in Listing 16, we use a renderer to
customize the data and to color individual items in cells before displaying
them.
In the beginning of this section, we showed how we made a call in
the createChildren function of the DynamicAdvancedDataGridCanvas to the constructor of
the DynamicAdvancedDataGrid to create the table. We
also showed how each time the table is refreshed, a recreated data provider is
assigned to the table. The action of assigning a data provider to a table
occurs in the DynamicAdvancedDataGrid class, within
the "set" function. This "set" function overrides the "set" function of the
base class. The last line of this function is a call to the createColumns function, which dynamically creates
columns in the table:
Listing 16. Renderer to customize the data
override public function set dataProvider(value:Object):void
{
super.dataProvider = value;
createColumns(dataProvider as ArrayCollection);
}
|
The
table's content, in the form of an array collection, is passed to the createColumns function as a parameter. The array
collection contains as many rows as will be displayed in the table. Each row
has as many column objects as the number of columns that will appear in the
table. The first thing we need to do in the createColumn function is to find
out how many column objects are in a row. This is done by simply counting the
number of objects in the very first row, which we pass as a parameter to the
getNumberOfProperties function in the DynamicAdvancedDataGrid class. As soon as we determine
the number of columns, we loop over this number and create the columns with
Flex's AdvancedDataGridColumn class. As you see in
the code fragment of the createColumns function
(Listing 17), the dataField of a column is assigned
an order number in which the column's object appears in the row of the data
provider's collection. We assign other properties such as "resizable" and
"visible" to each column and push the column into the array of columns, called
"theColumns." When the loop is over, we assign the array of columns to the
table's property "columns." This is done in the last statement of the createColumn
function.
Listing 17. DynamicAdvancedDataGrid class: createColumns function
private function createColumns(dp:ArrayCollection): void
{
if(dp.length > 0)
{
var numColumns:int = getNumberOfProperties(dp[0]);
var theColumns:Array = new Array();
for(var i:int = 0; i < numColumns; i++)
{
var dgc:AdvancedDataGridColumn =
new AdvancedDataGridColumn();
dgc.dataField = i.toString();
...
theColumns.push(dgc);
}
columns = theColumns;
}
}
|
Before
we go on to the final part of UI implementation, the custom item renderer, we
would like to point out that in the constructor of the DynamicAdvancedDataGrid class we assigned our custom item renderer
(the DynamicAdvancedDataGridItemRenderer wrapped in
a ClassFactory object) to the itemRenderer property of the table:
itemRenderer = new ClassFactory( DynamicAdvancedDataGridItemRenderer);
|
Item
renderers allow for the display of customized data. We created our own item
renderer to customize our data before showing it to the user. We based our
custom renderer on Flex's HBox component and
overrode the set function of this component. We modify the current row data
object after it gets passed to the item renderer.
As we've mentioned before, a content cell can have more than one item in its body. John can have his three favorite books, and Ivan can have his two favorite movies. Three items have to be displayed in a cell associated with "Favorite books" and "John," while two items have to be displayed in a cell mapped to "Favorite Movies" and "Ivan."
Therefore, we decided that each element in a cell
can be displayed with the help of what we call an "item viewer." An "item
viewer" is a "box" that contains a text field. The item renderer creates,
within its "set" function, as many item viewers in a cell as there are number
of answers found in the cell's data. The ItemViewer
class extends the Box class. It holds one UI
component, SuperUITextField, derived from the
UITextField.
Below is a fragment of the
code from the DynamicAdvancedDataGridItemRenderer
set function. Notice in the code fragment below (Listing 18) that the database
is queried for all answers that belong to a single content cell. After getting
the answers, item viewers are created in a loop over these
answers.
Listing 18. DynamicAdvancedDataGridItemRenderer class: fragment from "set" function
if(data && data is Row)
{
var cell:Cell = (data as Row).getCellAt(listData.columnIndex);
if (cell is NullCell) return;
if(cell is HeaderCell)
{
var viewer:ItemViewer = new ItemViewer();
viewer.styleName = header;
...
viewer.title = cell.data as String;
addChild(viewer);
}
else
{
// content cell
var parameters:Array = cell.data as Array;
var answers:Array =
DataBase.instance.getAnswers(parameters[1],parameters[0])
for each(var answer:String in answers)
{
var viewer:ItemViewer = new ItemViewer();
viewer.styleName = "content";
viewer.title = answer;
addChild(viewer);
}
}
}
|
The
last implementation detail we want to mention is the use of a viewer's styleName property, which is used to color and shape
the viewer. In the fragment of the code above, each viewer was assigned a
styleName. If a viewer happens to be created in
a header cell, the viewer's styleName will be
"header." Otherwise, the styleName is "content."
The background color, corner radius, and border and thickness styles of "header"
and "content" viewers are defined in class selectors of the external CSS file:
"example.css" (Listing
19).
Listing 19. Styling header and content viewers
.header
{
borderStyle : solid;
backgroundColor: #FFFFFF;
cornerRadius: 8;
borderThickness: 1;
}
.content
{
backgroundColor: #AABBCC;
cornerRadius: 8;
}
|
Now you have seen how we created our Flex-based juxtaposition table, and we hope you have had a chance to create your own custom perspectives by changing the horizontal and vertical data sets. You can apply these ideas and implementation solutions to your own applications, if those applications require the dynamic creation of columns, the display of customized data, or unique shaping and coloring of cells.
In the next article, we will show how we fluidly expand and contract items in cells and how we drag-and-drop items from one cell to another.
| Description | Name | Size | Download method |
|---|---|---|---|
| Sample code | DataGridDeveloperWorksExample1.zip | 631KB | HTTP |
| Sample code | DataGridDeveloperWorksBaseExample.zip | 507KB | HTTP |
| Sample code | DataGridDeveloperWorksExample1_Source.zip | 14KB | HTTP |
Information about download methods
- In the Flex 3 data
visualization package, you can find information for your Adobe Flex and Adobe AIR
user interfaces.
-
IBM
product evaluation versions: Download these versions today and get your hands
on application development tools and middleware products from DB2®,
Lotus®, Rational®, Tivoli®, and WebSphere®.
- Check out My
developerWorks: Find or create groups, blogs, and
activities about Web development or anything else that
interests you.





