In Part 1 of this two-part series, we introduced the Thin Client Framework (TCF). We provided a conceptual overview of the framework's purpose and foundations, outlined its most relevant components, and used a working example to demonstrate how those components work together in a TCF application. In this second part of the series, we'll offer a more detailed discussion of TCF, focusing almost entirely on the framework's programming model. We'll describe each of the major TCF components, explaining its role, function, and related data values. Code examples will be used to demonstrate how each component is used. Because the discussion here builds on the information relayed in the first part of the series, you should read Part 1 before continuing on with Part 2.
Before we launch into a detailed discussion of TCF's components, we'll take a moment to review the TCF architecture. Figure 1 illustrates the TCF architectural pattern. Ovals represent major TCF interfaces or classes, while rectangles or diamonds represent TCF support classes. All TCF communication is event based.
Figure 1. The TCF architecture

For a more in-depth explanation of the TCF architecture, see Part 1 of this series.
TCF is implemented as a small set of Java interfaces and classes, as shown in Figure 2:
Figure 2. TCF interfaces and classes

In the above list, the following key applies:
- Circle-I indicates an interface
- Circle-C indicates a class
- Superscript-A indicates an abstract class
- Superscript-F indicates a final class
As you can see, TCF consists of a relatively small number of components. The framework's power comes not from an extensive library of code but from its simplicity, combined with its generality and well-architected distribution of responsibilities. In the sections that follow, we'll describe most of the TCF component types illustrated in Figure 2, focusing on the details of how each one functions and is coded.
Java Thin Client (JTC) was the original iteration of TCF. Much of the
original JTC code remains in the current TCF implementation. For example,
the JTC interface is implemented by all the major TCF componentry. The
JTC interface yields the following benefits:
- JTC allows reference to all objects with a consistent type and behavior.
- A number of essential classes implement JTC. They are
AbstractViewController,AbstractApplicationMediator,AbstractTransporter, andAbstractDestination, as well as any top-level application classes. - The
getJTCs()method is particularly useful, in that it:- Ensures that each JTC implementation is expected to return all the JTC objects it creates
- Enables a runtime reconfiguration of the application
- Allows non-intrusive logging, tracing, and debugging
The Factory class is used to load and create shared and
singleton objects. For example, in a single JVM, you may want to
create and share one instance of a Transporter across
multiple locations. To achieve this, you would code each
transporter-instance location as follows:
Transporter t = Factory.newInstance(
"com.ibm.jtc.DefaultTransporter", true);
|
In this case, the class name is implicitly used as a key. The true value indicates that a singleton should be created. In addition to the class name, you can add a custom key, as shown below:
Customer c = Factory.newInstance(
"CUST", "com.abc.Customer", true);
|
The Factory class delegates its implementation to the
FactoryImpl class. To change the behavior of the
Factory
class, you simply subclass FactoryImpl and override
its non-static methods. From there, you need only create an instance of
your subclass and call the static method
Factory.setInstance(yourInstance). The static methods in
the Factory class will then call the instance methods of
your factory instance.
JTCEvent is the superclass for all TCF event classes.
It extends java.util.EventObject. In addition to the
inherited source field, JTCEvent provides the following fields:
- major: An int to describe the type of event; defaults to -1
- minor: An int to describe a variant on the type of the event; defaults to -1
- isConsumed: A boolean; true indicates that the dispatching to subsequent handlers
(that is, listeners) should stop
- data: An object; a generic reference that is carried along to listeners;
often a collection class (such as a
VectororHashtable) or a customized data object
Each JTCEvent subclass will define static final values for the
major and minor fields that represent its purpose.
ViewEvent, a subclass of JTCEvent, is the
mechanism for communication between
ViewControllers and ViewListeners.
ViewEvent provides many predefined constants
(too many to show here) for typical GUI-input sources such as buttons.
A ViewController typically converts AWT events such
as button presses into ViewEvents. The ViewEvents
are then processed, often by the ApplicationMediator.
Let's take a look at a typical event scenario. The code samples
below show how ViewControllers and
ApplicationMediators interact.
The ViewController first listens for the GUI event:
nextButton.addActionListener(this); |
Upon being called back by an AWT event (in
this case from nextButton) in the ActionListener
interface, the ViewController
creates and fires a ViewEvent to process the next action:
public void actionPerformed(ActionEvent ae) {
if (ae.getSource() == nextButton) {
ViewEvent ve = new ViewEvent(this);
ve.setMajor(ViewEvent.NEXT);
fireViewEvent(ve);
}
}
|
ViewEvents are received by the ViewListener.
The following code samples demonstrate a typical usage of the ViewListener.
Typically, the role of ViewListener is implemented in
an ApplicationMediator. First, the
ApplicationMediator
adds itself to the ViewController
as a ViewListener:
public void init(){
//...
detailsViewController.addViewListener(this);
}
|
Upon being called back in the viewEventPerformed method,
the ApplicationMediator decodes and processes the
ViewEvent:
public void viewEventPerformed(ViewEvent ve) {
switch (ve.getMajor()) {
case ViewEvent.NEXT:
// perform next action
break;
case ViewEvent.OK:
// perform OK action
break;
:
}
}
|
RequestEvent, a subclass of JTCEvent,
is used to represent a lightweight transaction or single-step service request. In
addition to the rather general major and minor values
inherited from JTCEvent, RequestEvents provide two additional java.lang.String values: family and command.
These values provide the specific event identification RequestEvents
typically require. The family value is used for routing to Destinations and the command value indicates the desired function. The
major and minor values can still be used to modify a command code.
The code samples below illustrate a typical usage of a RequestEvent.
Create a RequestEvent:
RequestEvent re = new RequestEvent();
re.setFamily("Loans");
re.setCommand("SubmitCustomerInfo");
|
Fire the event to a registered RequestListener:
try {
fireRequestEvent(this, re); // asynchronous
-- or --
fireRequestEvent(re); // synchronous
}
catch (RequestException e) {
// **** process any exception ****
}
|
Note that a synchronous request is processed on the calling class's thread. Synchronous processing should be used only for events that require minimal and rapid processing. An asynchronous request is processed on a different thread. Asynchronous processing allows more time for each event without tying up the requesting thread. Asynchronous processing leads to increased GUI responsiveness.
An asynchronous request calls back the RequestEvent originator in the
RequestResponseListener interface, as shown here:
public void requestResponse(RequestEvent re) {
// **** some success action ****
}
public void requestException(RequestException e) {
// **** some failure action ****
}
|
(A synchronous request would immediately either return or throw
a RequestException.)
ViewController is an interface that defines a reusable
user interface component (often a subclass of
java.awt.Component such as a javax.swing.JPanel) as a
part of an overall client-application GUI. Application authors
typically subclass AbstractViewController to implement a part
of the user interface, such as an input screen or dialog. It is best
practice to keep ViewControllers simple (that is, just a few fields and
buttons) so that they are reusable and can easily be composed into more complex GUIs. Figure 3 shows the ViewController runtime:
Figure 3. The ViewController runtime

ViewController provides the following methods:
- add/setViewListener manages a list of
ViewListeners. - getComponent returns the
java.awt.Componentthat theViewControllermaintains through either an is-a or a has-a relationship. - set/isEnabled accesses the
ViewController's enabled state. If enabled, theViewControllerwill respond to input events. - isValid accesses the
ViewController's valid state. The valid state is generally related to the application of validation rules to fields of theViewController. - set/isVisible accesses the
ViewController's visible state. Only visibleViewControllers can be seen. - refresh updates the
ViewControllerwith new data. - setValidationLevel indicates when the
ViewControllershould applyValidationRules. - setResources updates the
ViewControllerwithjava.util.ResourceBundles.
ViewControllers can be implemented in two ways:
through inheritance or through delegation. Both are shown here:
Inheritance (Java extends 'is-a'):
public class ViewControllerBase extends JPanel implements ViewController {
public Component getComponent() { return this; }
}
|
Delegation (Java variable 'has-a'):
public class ViewControllerBase implements ViewController {
//this can be a Panel, JPanel, Component, etc.
Component comp = new ...();
public java.awt.Component getComponent() {
return comp;
}
public void setEnabled(boolean e) {
comp.setEnabled(e);
}
public void setVisible(boolean v) {
comp.setVisible(v);
}
:
}
|
For TCF users who do not have access to the TCF source, but who wish
to change the default ViewControllerBase implementation
(which extends javax.swing.JPanel), the TCF runtime
package includes the following AbstractViewController
implementations:
- AbstractViewController extends
ViewControllerBase. (AVC is maintained for legacy TCF use and for those who have access to the source.) - AbstractObjectViewController extends
java.lang.Objectand uses delegation. - AbstractPanelViewController extends
javax.swing.JPaneland uses inheritance.
Every ViewController implementation extends either
AbstractPanelViewController or
AbstractObjectViewController.
As a result, ViewController can be used as
visual components (typical) or as other classes, as needed.
ValidationRule is an abstract class for a rule engine used to
validate and format ViewController field content. Validation is
usually used for user entry on a single text field or groups of text fields.
For example, you might set up validation for a range of numbers such as
a date or social security number. Other uses for validation include addresses,
lengths, and other business-specific formats. ValidationRule throws
a ValidationRuleException upon failure. TCF provides hints for the
ViewController to use via the
setValidationLevel method on the
AbstractViewController class.
Using ValidationRule keeps validation
processing separate from the data model and ViewControllers.
Rather than having each object maintain its own validation, you merely encode
the correct validation rule for each object. As a result, each
component is more generic, and far more reusable. A ValidationRule
can use inheritance or delegation chaining. As a result, you
can code each ValidationRule for simplicity and increased function.
ValidationRule has two primary methods:
edit() and normalize();
each one is shown in the code samples below.
edit() maps an input string to a human-friendly formatted form:
//validate and redisplay
String value = textfield.getText(), result = null;
try {
result = socialSecurityVR.edit(value);
}
catch (ValidationRuleException e) {
result = **** fix here ****;
}
textField.setText(result);
|
normalize() maps an input string to a minimal, computer-convenient form.
The result of normalize is often used as a data-model value, or as a
RequestEvent parameter sent on to a Destination:
//validate and update the data objects
String value = textfield.getText(), result = null;
try {
result = socialSecurityVR.normalize(value);
}
catch (ValidationRuleException e) {
result = **** fix here ****;
}
dataObject.setText(result);
|
Here's an example of a rule execution:
edit("123456") -> $1,234.56
normalize("$1,234.56") -> +00123456 (as Decimal(8,2))
edit("12345x") -> ValidationRuleException
|
ApplicationMediator is an interface that defines the control
logic of an application.
ApplicationMediators can be nested
(that is, implemented as hierarchical controllers).
ApplicationMediators accept new data with the
refresh() method,
passing the data on to one or more contained ViewControllers or
ApplicationMediators.
ApplicationMediator provides the following methods:
- add/remove/fireEvent sends events to associated
PlacementListeners,ViewListeners,RequestListeners, andTopListeners. - set/isEnabled accesses the
ApplicationMediator's enabled state. Enabling anApplicationMediatoralso enables any containedViewControllers andApplicationMediators. - setResources passes
ResourceBundles to containedViewControllers andApplicationMediators.
Figure 4 shows the ApplicationMediator runtime:
Figure 4. The ApplicationMediator runtime

Because users will often change their minds about how the desktop application should look,
it is important to ensure that components on the GUI can be easily changed and moved.
PlacementListener is an interface that manages the placement
of ViewControllers on the screen.
In TCF, the following functions are separate, in that each one is handled by a different TCF component:
- View creation (
ViewController) - View ordering (
ApplicationMediator) - View placement (
PlacementListener)
Note that ApplicationMediators control the ordering of
ViewControllers, but not their screen placement.
The separation of functions increases the reusability of ViewControllers and
ApplicationMediators, since they have no placement-related
function of their own.
The PlacementListener runtime is shown in Figure 5:
Figure 5. The PlacementListener runtime

Here is an example usage of a PlacementListener,
as typically contained (coded) in an ApplicationMediator:
PlacementEvent pe = new PlacementEvent(this, customerVC, PlacementEvent.ADD); firePlacementEvent(pe); |
The following usage, typically contained in the main class of your application,
shows an example placementEventPerformed method implementation from the
PlacementListener interface.
public class MyApplication implements PlacementListener {
public void placementEventPerformed(PlacementEvent pe) {
switch (pe.getMajor()) {
case PlacementEvent.ADD:
if (pe.getSource() instanceof AM1)
panel1.add( pe.getComponent(), BorderLayout.CENTER);
else
panel2.add(pe.getComponent());
break;
:
}
}
|
TopListener is an interface used to separate business- or
environment-specific responsibilities from other TCF componentry.
Again, the separation of responsibilities increases the reusability of TCF components.
You use TopListeners when other services providers, such as RequestListeners and Destinations, aren't
appropriate. TopListener functions could
include:
- Launching other applications
- For Java applets, accessing the browser
- Displaying system-level messages
- Setting the GUI's title
- Issuing security checks
- Exiting the JVM
Figure 6 shows the TopListener runtime:
Figure 6. The TopListener runtime

And here's an example use of a TopListener
(typically from the main class of your application):
ApplicationMediator am = new MyApplicationMediator(); am.addTopListener(this); |
Or, in the ApplicationMediator:
TopEvent te = new TopEvent(this, TopEvent.STATUS, 0, "Loading files..."); fireTopEvent(te); |
Later in the TopListener callback implementation of the TopEventListener interface:
public void topEventPerformed(TopEvent te) {
switch(te.getMajor()) {
case STATUS:
browser.setMessage((String)te.getData());
break;
:
}
}
|
In TCF, all data is passed as a parameter to some event type (that is,
ViewEvent, RequestEvent,
TopEvent,
and PlacementEvent) as the data value (a Java object)
or as the parameter of a callback method, refresh().
In general, data flows up, in events, toward
the data processor, or down, via refresh, toward the event
generator. During this flow the data object may be changed (typically
modified or added to) by the components it flows through.
Figure 7 shows the data model runtime:
Figure 7. The data model runtime

Here's how a ViewController might respond to a refresh call using a
java.util.Map data model:
public void refresh(Object data) {
if (data != null) {
refresh((Map)data);
}
}
void refresh (Map data) {
nameField.setText(data.get("CustomerName"));
idField.setText(data.get("CustomerId"));
repaint();
}
|
Alternate data models are easily supported, as shown here:
void refresh(Object data) {
if (data instanceof Vector) {
refresh((Vector)data);
} else if (data instanceof Map) {
refresh((Map)data);
} else if (data instanceof DOM){
refresh((DOM)data);
} ...
}
|
Typically, a ViewController would update any fields
with the new data values and re-enable itself to accept new input
(which would, in turn, cause it to generate ViewEvents).
ApplicationMediators generate requests for service by
creating and firing RequestEvents. RequestEvents
are processed by implementers of the RequestListener interface.
If a request cannot be processed, a RequestException is thrown.
Figure 8 illustrates a typical event-processing scenario:
Figure 8. An event-processing scenario

TCF provides built-in threading support. This can greatly simplify
the implementation of ViewControllers,
ApplicationMediators, and Destinations. Event dispatching is supported in
the implementations of AbstractApplicationMediator, AbstractTransporter, and AbstractDestination. In general, asynchronous
dispatching is required in only one of these implementations. AbstractTransporter is often the most convenient
choice. Please note that synchronous processing is the default; to
use asynchronous processing, you must enable it in the RequestEvent.
Figure 9 shows how event dispatching is supported in
AbstractApplicationMediator;
the other implementations are very similar.
Figure 9. The ApplicationMediator event dispatcher

TCF provides two implementations of the RequestListener interface:
a Transporter and a Destination.
The role of a Transporter is to select the appropriate Destination(s)
to process a given request. The DefaultTransporter implementation
provides this support.
Any particular RequestEvent can be forwarded to one or several Destinations;
multicasting is done in order of Destination registration and continues
until all Destinations have processed the request or a
RequestException is thrown.
Figure 10 shows the Transporter runtime:
Figure 10. Transporter runtime

Typically, Transporters map RequestEvents to
RequestListeners, particularly Destinations.
Here's an example Transporter setup:
Transporter t = new DefaultTransporter(); ApplicationMediator am = new MyApplicationMediator(); am.addRequestListener(t); |
Here, the ApplicationMediator would fire
RequestEvents to
all RequestListeners (for example, the Transporter);
for example:
String family = "Service", command = "execute";
RequestEvent re = new RequestEvent(this, family, command);
try {
fireRequestEvent(re);
}
catch (RequestException e) {
// **** process as needed ****
}
|
The Transporter would then forward the
RequestEvent to any Destinations registered for that family of RequestEvent. Destinations are added to the Transporter as RequestListeners. They are added with a family value
that defines the RequestEvents they are interested in receiving.
For example:
EJBDestination ejbDestination = new EJBDestination();
Transporter t = new DefaultTransporter();
ApplicationMedaitor am = new MyApplicationMedaitor();
am.addRequestListener(t);
t.addRequestListener("Product", ejbDestination);
|
This approach allows you to multicast Destinations
for a single RequestEvent. For example, in the code
below, the RequestEvent will be sent to each listed
Destination in the order indicated by its priority
value.
Transporter t = new DefaultTransporter();
Destination d;
d = new WildDestination(); // low priority
t.addDestinationListener(Transporter.WILDCARD, d);
d = new EJBDestination(); // normal priority
t.addDestinationListener("Loans", d);
d = new PriorityDestination(); // high priority
t.addDestinationListener(Transporter.PRIORITY, d);
d = LoggerDestination(); // by position (often last)
t.addDestinationListener(Transporter.DONT_CARE, d);
|
A Destination is representative of any service the TCF client needs,
including services outside the client (such as network actions) or
services local to the client (such as file-system access). A
Destination can be simple or complex, and can
be appropriate to a thin or a thick client.
Figure 11 shows the Destination runtime:
Figure 11. Destination runtime

Destinations handle the following TCF functions:
- Locate the server or data resources
- Select network transport method, choosing among low-level protocols
such as sockets, RMI, URLs, files, and so on
- Select server type, choosing among APIs such as EJBs, servlets, Web servers,
legacy, and so on
- Select message format (that is, translate
RequestEvents and data into server formats) - Process time-outs
- Handle retries
- Perform caching
- Perform polling for sessions or printing
- Perform logging (that is, save pre- and post-
RequestEvents)
Although it is intended to support the development of Java GUI-based
applications, TCF is not limited to only this use. It has also been
applied in Web-based applications. For example, it is quite practical
to use the TCF back-end classes (from the Transporter onward)
in server-side code. A servlet or JSP file can be coded to generate
RequestEvents and fire them to the
Transporter
just as easily as an ApplicationMediator does. In
fact, we know of an Apache Struts-based application where the
ActionServlet is set up to
fire RequestEvents
(see Resources).
Another approach is to replace the Java-based GUI with an
HTML-based one. To do this you create servlets or JSP files that
fire ViewEvents in their doGet/doPost
methods. These events are processed by ApplicationMediators as
usual. The resulting information, provided by the refresh() method, controls the returned HTML
generated to the servlet's or JSP file's out() stream.
If the ViewController servlets are designed to only create a
small part of the full page, they can be composed into a larger page
by use of a PlacementListener. For HTML this would be a servlet
that builds a TABLE that includes, as table data cells, the results from
the other ViewController servlets that compose the GUI. By careful
design, it should be possible to build a TCF application that has
both a Java- and an HTML-based user interface. In this case, you
could completely reuse the code from the ApplicationMediator onward,
so you would only have to custom code the ViewControllers and
PlacementListeners.
In this two-part series, we have provided a beginner's introduction to the Thin Client Framework architecture, design, and implementation. We have discussed the conceptual underpinnings of the framework, showed you how the TCF architecture is put together, and discussed the most relevant components of the framework in detail, from both a high-level and a programming perspective. We have also introduced a TCF implementation, Example04, to let you see for yourself the function and coding of the various components. In closing, we have suggested a number of ways TCF can be extended beyond its original intended use.
- See the Jakarta Project homepage to
learn more about Apache Struts.
- Find hundreds of articles about every aspect of Java
programming in the developerWorks Java technology
zone.
- See a complete listing of free Java technology tutorials from developerWorks on the developerWorks Java technology
tutorials page.
Dr. Peter Bahrs is an IBM Distinguished Engineer and IT Architect in the IBM Software Services organization. Dr. Bahrs specializes in large e-business systems development in the financial services industries. He holds several patents and has spoken at industry conferences such as JavaOne. Dr. Bahrs is currently on assignment to IBM Zurich, Switzerland. He can be reached at tcfhelp@us.ibm.com.

Dr. Barry Feigenbaum is a member of the IBM Worldwide Accessibility Center, where he serves as a member of a team that helps IBM make its products accessible to people with disabilities. Dr. Feigenbaum has published several books and articles, holds several patents, and has spoken at industry conferences such as JavaOne. He serves as an Adjunct Assistant Professor of Computer Science at the University of Texas, Austin. You can contact Dr. Feigenbaum at feigenba@us.ibm.com.




