Chapter 32

Encapsulating Legacy Systems

by Mark Wutka


Keeping up with technology is one of the constant problems that most businesses face. You buy a top-of-the-line database system and a year later, after you've spent considerable time converting your organization over to the new database, the database is obsolete. If you try to keep your systems on the leading edge all the time, you'll probably spend more time and money changing systems than you do using them. More likely, you'll keep the old system for a long, long time. Most of these older systems are referred to as "legacy" systems, and while they may not be on the forefront of technology, they are still the lifeblood of many businesses.

By the time you decide to switch systems, all your applications are so heavily tied to the legacy system that you have to rewrite all your applications, making it even more expensive to switch. You have to factor these costs into your decision to switch. You don't switch just to use new technology. You switch because a new system will save you money. If you are more heavily tied to the legacy system, you are more willing to keep using it, because the cost of switching is more than the cost of maintaining the existing system. It is obvious that you could reduce the cost of upgrading and changing systems if you could design your applications so they weren't as dependent on specific products.

As you design new applications, you don't want your design to be constrained by the limits of the current system. Obviously, the implementation will have these constraints, but you want to leave room to grow. Rather than attacking this problem on a per-application basis, take a step back and look at the systems you have today, and try to put a prettier face on them.

You can use a technique called encapsulation to make a legacy system look like a newer system. There is no magic here, since you can't really make the old system work just like a newer one. It won't run any faster, or magically perform some new functions. What encapsulation does is break your dependence on the exact interfaces of the legacy system. For example, suppose you have an MVS system from IBM, and if you are at a large corporation, you probably do. While MVS is expanding its accessibility, you still have some limitations. Suppose, for instance, that you don't have an MVS TCP/IP gateway available and you must use IBM's proprietary SNA protocol to access MVS. How are you going to write an applet to access MVS data? You aren't going to find a copy of the Netscape Navigator that comes with SNA built in. What you can do, however, is write a program that sits in between MVS and the applet and does the necessary translation. Figure 32.1 illustrates an example configuration.

Figure 32.1 : An encapsulation program puts a more friendly face on a legacy system.

Focusing on Function, not Form

One of the biggest traps you run into when writing an application is focusing only on the exact form of the data you can get or the exact commands that you can give to the legacy system. What you want to do instead is concentrate on what the legacy system actually does.

For example, suppose you have an ordering system and you want to create a Java applet that can place orders. Assume that you have the following commands on your ordering system:

As you can see by this set of commands, the ordering system requires a high degree of interaction. This set of commands is the "form" of the legacy system. Its function is that it creates orders of parts for your customers. That may seem like a subtle point, but it is very important. One of the reasons we spend so much money re-engineering old applications is that they were too heavily tied to the form of the legacy system.

If you got a new ordering system, the likelihood of the commands being different is fairly high. Most systems use their own set of commands. The function should still be the same for a new system, however. You'll still be using a system to create orders. If you focus on the function of the system and don't worry as much about the actual commands, your applications will adapt to new systems more readily.

Of course, there is a trade-off here. You have to do a little extra work to translate from the legacy system to the application. That's where the encapsulation comes in.

You could, for example, create an encapsulation that placed an entire order at once. It would have methods to list the available parts and place an order. The encapsulation program would translate the order placement into the series of commands expected by the legacy system. Figure 32.2 illustrates how this might take place.

Figure 32.2 : An encapsulation can present an interface different from the legacy system.

This form of encapsulation is actually a design pattern known as a "facade."

Providing Access to New Systems

One of the reasons you encapsulate legacy systems is that you want to move to a more modern computing platform. Sometimes the move is on the server side, and sometimes the move is on the client side. For instance, you may want to keep your old mainframe, but upgrade some or all of your workstations to inexpensive Web terminals. Sometimes you want to upgrade the central server or mainframe, but you can't afford to upgrade all your terminals out in the field.

You can often use encapsulation to facilitate a system upgrade. For instance, an airline has a large network of very old terminals, the kind you see every day at the airport. These terminals are connected via a network to a group of mainframes. Now, you realize that it is much more cost effective to develop applications on a UNIX or NT system. In fact, many of your users already have workstations that support a nice graphical interface. Unfortunately, you still have to support the users out in the field, with their old-fashioned terminals. Do you develop your new application on the old mainframe, or do you aim for the new technology? Figure 32.3 illustrates this dilemma.

Figure 32.3 : It is difficult to decide whether to extend the legacy system, or put an application on a new system.

Ideally, the answer to this problem is that you aim for the new technology. Wherever you have to use your legacy technology, encapsulate it and put a pretty face on it.

The practice of separating the application from the user interface is really going to shine for you when you have to deal with legacy systems. If you design your application as you would like it to be, rather than constrain it by the way things are, you can make the existing system fit.

While "vision" is an overused term these days, you need vision when you design applications. You need to be able to look beyond what you have right now and see where you want to be, then work toward that goal. You'll never escape the legacy system if you keep designing it into your new applications.

You design your application the way you want, then adapt the legacy systems to fit your application, not vice versa. For example, suppose you design a new e-mail system using Java for both the client and server. Figure 32.4 illustrates a possible configuration.

Figure 32.4 : A Java e-mail system.

Now that you have your application designed, you concentrate on encapsulating the legacy system to fit into the new application structure. In the case of the e-mail system, you may want to allow the old legacy terminals to access the system. You create an object or a server that looks like a legacy host to the terminals, but looks like a client to the e-mail system. This object would translate the new-user interface into something the legacy terminals understand. Figure 32.5 shows an example of this.

Figure 32.5 : You can translate a new-user interface into something a legacy terminal understands.

You could take a different approach with the new application. Suppose you want to use the existing e-mail application on the legacy system. Maybe it is too expensive or difficult to get the legacy terminals to access the new e-mail application. You could create an object that translates the old e-mail interface into something that looks like the new application, as far as the clients are concerned. Figure 32.6 illustrates this configuration.

Figure 32.6 : You can translate a legacy interface into something newer clients can deal with.

In both of the previous examples, the encapsulation was based on the clear separation between the application and user interface. Once you separate them, you can translate different application interfaces and user interfaces into something that fits your design.

Using CORBA to Open Up a Closed System

Once you come up with a way to get data out of your legacy system, you should make it available to other applications using a mechanism that is likely to be supported by many systems. CORBA is a reasonable choice for this, since it is a well-known standard that has been growing in popularity for the last few years. Even if Java's popularity suddenly wanes in favor of something else, you still have a CORBA interface that you could access from other languages.

Figure 32.7 shows some of the ways you can access a legacy system if the encapsulation provides a CORBA interface.

Figure 32.7 : CORBA expands the accessibility of an encapsulated system.

Encapsulating a TCP/IP System

If your legacy system can be accessed over the network via TCP/IP, it will be easy for you to access the legacy system. This doesn't mean that it will be easy to write the encapsulation. You may wonder why you even need extra code to encapsulate the system when an applet could make a TCP/IP connection straight to the legacy system (assuming the security restrictions didn't get in the way). Remember that you aren't just encapsulating the access to the system, you are encapsulating the functions it provides.

You don't want to tie your applet to the specific interface provided by the legacy system. This way, if you upgrade the legacy system, you change only the encapsulation, but the applets that talk to the encapsulation don't need to change.

Encapsulating with Native Method Calls

Even if your legacy system can run Java, you may still need to create an encapsulation layer. Java may not be able to access the applications running on the legacy system. You can create native methods to handle these situations.

You also may need native methods if you have a special interface card or special interface software to access your legacy system. For example, many IBM systems can only be accessed using the SNA networking protocol. You occasionally need a special interface card to talk to these systems, but many times you just need special networking drivers. Either way, you need a native method to access the special card or drivers.

Figure 32.8 illustrates a typical native method encapsulation.

Figure 32.8 : You often need native methods to access a legacy system.

Wrapping Java Around a Native Interface

Suppose your ordering system came with a C library that let you perform all the functions you needed. You could start and end transactions, get a parts list, and add and remove parts from an order. You could create a Java class to access the C library via native method calls.

Listing 32.1 shows a Java interface to the ordering system, with native method calls to the C library.

Listing 32.1  Source Code for
package ordering;

// this class provides an interface to an ordering system. It makes
// calls to native C methods which take care of accessing the real
// system.

public class Ordering extends Object
// transactionID is the transaction id returned by the C interface
// to the ordering system. Each call to the ordering system must
// be accompanied by the transaction ID.

     int transactionID;

// Create an instance of an Ordering object, which begins an ordering
// transaction.

     public Ordering(String customerId)
     throws OrderingException
          transactionID = startTransaction(customerId);

// Add a part to the current order

     public void orderPart(String partNumber, int quantity)
     throws OrderingException
          orderPart(transactionID, partNumber, quantity);

// remove a part from the current order

     public void removePart(String partNumber)
     throws OrderingException
          removePart(transactionID, partNumber);

// finish the order

     public void endTransaction()
     throws OrderingException

// abort the order

     public void abortTransaction()
     throws OrderingException

// These methods are implemented in a local DLL

     protected native int startTransaction(String customerId)
          throws OrderingException;

     public native static String[] listParts();

     protected native void orderPart(int txnID, String partNumber,
          int quantity) throws OrderingException;

     protected native void removePart(int txnID, String partNumber)
          throws OrderingException;

     protected native void endTransaction(int txnID)
          throws OrderingException;

     protected native void abortTransaction(int txnID)
          throws OrderingException;

The methods in the Ordering class are not quite a straight pass-through to the real ordering system. Rather, it provides a slightly higher level of abstraction. It considers an Ordering object to be a transaction, and hides the transaction ID from the users of the Java class. You will find that many C libraries return information that you must use for future function calls. You don't actually use the information. Instead, since the library has no way of grouping method calls and data together, like an object does, the library makes you handle the data and forces you to pass it to whatever functions need it. When you design a Java interface to a native library, keep information like this hidden within the Java class. Don't let the public methods pass back anything that isn't useful.

Writing Native Methods in C

Once you have defined a Java class with native methods, you must write the native methods yourself. You can't simply call any arbitrary C function from Java, you almost always have to create a C function that calls the real function you want.

The reason you can't call any arbitrary C function is that native Java methods must conform to a strict naming scheme that combines the method name, the Java class name, and the package name. For example, the declaration for the native C endTransaction method would look like this:

void ordering_Ordering_endTransaction(
      struct Hordering_Ordering *thisPtr, long txnID)

Java always passes a "this" pointer to its native methods, which precludes the use of calling any old C function as a native method. Typically, your native methods will turn around and call other C functions, however.

For instance, your ordering_Ordering_endTransaction function would probably call the endTransaction function in the C library that came with your ordering system.

When you create a Java class with native methods, you have to generate a special header file that contains declarations for the C implementation. You create the header using the javah command. Once the Ordering class has been compiled in Java, you issue the following command:

javah ordering.Ordering

This will generate a header file called ordering_Ordering.h. Next, you must generate a set of stubs, which Java uses to invoke your native methods. You also use the javah command for this, but you must include the -stubs option:

javah -stubs ordering.Ordering

This will create a C file called ordering_Ordering.c. You must compile and link this file into a shared library, along with your native methods. Listing 32.2 shows a skeletal implementation of the native methods for the Ordering class.

Listing 32.2  Source Code for orderingImpl.c
#include "ordering_Ordering.h"

/* This is an absolutely skeletal implementation of the
   native methods for the Ordering class. The only method
   that does anything is the listParts method, which returns
   an array of strings. The rest of the methods are dummies.

int nextId = 1;          /* For returning different transaction ID's */

/* parts is a list of values that will be returned in listParts */

char *parts[] = {
     "12345 Widget",
     "23456 Deluxe Widget",
     "55534 Thing",
     "30038 Zippy"

struct Hjava_lang_String;
long ordering_Ordering_startTransaction(struct Hordering_Ordering *thisPtr,
     struct Hjava_lang_String *customerId)
     return nextId++;

HArrayOfString *ordering_Ordering_listParts(struct Hordering_Ordering *thisPtr)
     HArrayOfString *retval;
     ClassArrayOfString *strs;
     int i;

/* Create an array of strings that will contain 4 strings */
     retval = (HArrayOfString *) ArrayAlloc(T_CLASS, 4);
/* If we couldn't allocate the memory, throw a Java exception */
     if (retval == NULL) {
         SignalError(EE(), "java/lang/OutOfMemoryException", NULL);
         return NULL;
/* ArrayAlloc allocated an array of objects, this call makes it an
   array of strings */

    unhand(retval)->body[4] = (HString *)
        FindClass(EE(), "java/lang/String", TRUE);
/* Get a pointer to the array of strings */
     strs = unhand(retval);

/* Fill the array of strings with Java strings */

     for (i=0; i < 4; i++) {
          strs->body[i] = makeJavaString(parts[i], strlen(parts[i]));

     return retval;

void ordering_Ordering_orderPart(struct Hordering_Ordering *thisPtr,
     long txnID, struct Hjava_lang_String *part, long quantity)

void ordering_Ordering_removePart(struct Hordering_Ordering *thisPtr,
     long txnID, struct Hjava_lang_String *part)

void ordering_Ordering_endTransaction(struct Hordering_Ordering *thisPtr,
     long txnID)

void ordering_Ordering_abortTransaction(struct Hordering_Ordering *thisPtr,
     long txnID)

The skeletal implementation in Listing 32.2 doesn't interface with a real ordering system. Chances are, a real system wouldn't have exactly these methods. For one thing, there are too many things left out. However, for the purposes of illustration, assume that there really is a system that uses the above methods. Once you can access a legacy system, you can change the way it is accessed, as you will see later in this chapter in the section titled "Presenting a Different Interface."

Encapsulating by Emulating a User

Sometimes, the only way you can access a legacy system is by interacting with it as if you were a user. This method is more commonly known as "screen-scraping." You basically parse the individual output screens of the legacy application, and generate input to it as if you were a user.

This type of encapsulation is often combined with other methods. For instance, your legacy system may support TCP/IP, but you have to do screen-scraping to get the information from the system, as illustrated in Figure 32.9.

Figure 32.9 : Even when a legacy system has TCP/IP, you may have to resort to screen-scraping.

Sometimes, the only access you can get with a native method is as a terminal to the application, and not directly into the application itself.

You may have to sink so low as to connect up via a modem or a serial cable to do asynchronous communications. You would almost certainly need native methods to do this, although under UNIX, you may be able to get away with doing file I/O. Figure 32.10 illustrates this kind of configuration.

Figure 32.10: Sometimes, the only way to get data from a legacy system is via a modem or serial connection.

Getting Assistance from the Legacy System

There's no rule that says that the legacy system can't have an active role in the encapsulation. Many times, you can add code to the legacy system in a few strategic places and open up the system to encapsulation. You can often create simple messaging protocols to pass data to and from the legacy system without having to resort to scraping screens.

Presenting a Different Interface

The remnants of a legacy system can stay around long after the system itself is gone. Many times, applications that originally interfaced with the legacy system still do things the way the legacy system did, even though there is no reason for it.

This situation occurs outside the computer industry as well. A woman was preparing a ham for a family dinner when her daughter asked her why she cut the end off the ham. The woman replied that her mother always did it. The woman then asked her mother why she always cut the end off the ham. Her mother replied, "So it would fit in the pan."

If you are encapsulating a legacy system, you have the ability to change the interface into the system to some degree. If the legacy system does something in a strange way, use the encapsulation to hide it.

Take the ordering system shown previously, for example. When you create an order, you have to begin a transaction, add parts, then either end or abort the transaction. Maybe you don't want to work that way anymore. You might be better off building an order at the client side, then have the client send the entire order, which is then fed into the system.

This would be more in line with the way Web services work. You try to do everything in one message, since there is no concept of a session on a Web server. Of course, designing your system to work only as a session-less system may also be a bad thing. Do what makes sense for your application.

You can use this technique when writing an application to use an interface that has not yet been completely specified or is changing. You decide on the interface your application will use, and write your application. When the real interface is completed, write an object that translates from one interface to the other.

The ordering system is a good candidate for a different interface. You can easily create an object that lets you place orders in a single, session-less method call, rather than the group of calls in the current interface.

Listing 32.3 shows a simple class with a static method that places an order using the Ordering class.

Listing 32.3  Source Code for
package ordering;

public class Orders extends Object
     public static void placeOrder(String customerID,
          PartOrder[] parts) throws OrderingException
          Ordering ordering = new Ordering(customerID);

          for (int i=0; i < parts.length; i++) {


Listing 32.4 shows the PartOrder class, which encapsulates the information for a single entry in the ordering system.

Listing 32.4  Source Code for
package ordering;

public class PartOrder extends Object
     public String part;
     public int quantity;

     public PartOrder()

     public PartOrder(String part, int quantity)
          this.part = part;
          this.quantity = quantity;

As you can see, the placeOrders method is much easier to use than the regular Ordering object. In a typical ordering system, this simplified interface may be a disadvantage. If this were an airline reservations system, you would have to book all your seats at once and hope that no one got a seat you wanted before you did. When you have a session-based system, you reserve the seats (or the parts) as your transaction progresses. If you abort the transaction, the things you have reserved get freed up for someone else. You will have to decide if you are willing to give up this functionality. It may or may not be worth the cost of a simplified interface.

Combining Multiple Systems

One of the more interesting aspects of encapsulation is combining various pieces to present a single, uniform system. It is a task that is both interesting and challenging. A simple example of this might be adding additional customer information to a customer database. For example, suppose your customer database stored the customer's account number, name, shipping address, phone number, and fax number. Now, suppose you have created a new shipping system that allowed customers to get software from you via ftp. You want to add information to the customer database to support this.

If would be nice if you could just add all this information to your legacy system, but maybe that wouldn't be the best thing. Maybe it is so difficult to get data out of the legacy system quickly that your ftp server couldn't service all the requests in a reasonable time.

You can create a separate database containing the information for the ftp server. Your encapsulation would then store information in both the ftp database and the legacy database. Whenever you retrieve information, the encapsulation queries both systems for the information you need.

Some systems have the ability to maintain information across multiple databases. You should see if your legacy system supports this kind of thing on its own before you attempt to write it yourself. That's a good idea in any situation. A good transaction processing (TP) monitor will also handle this kind of situation.

Figure 32.11 shows an encapsulation that stores data in two places.

Figure 32.11: An encapsulation can make multiple databases look like one.

There can be some serious drawbacks to this approach, so you need to be very careful if you decide to try it.

Handling Deletions Originating in the Legacy System

If the encapsulation provides the only access to the legacy system, you can control things a lot more easily. If the legacy system is still accessed through some other means, like legacy terminals, you can get into some serious synchronization problems. If someone removes a customer from the legacy database, the other databases don't know about it. If you require all the databases to be in absolute synchronization at all times, this can be a major pain.

You can alleviate this problem by always checking with the legacy database to make sure a record exists. For instance, suppose you removed a customer from the legacy database for some reason, and then that customer tried to access the ftp server. Even though all the information necessary for the ftp is stored in the other database, the encapsulation still checks with the legacy system and learns that the customer account is no longer there.

The problem with this kind of approach is that it is slow. The other database may be a lot quicker than the legacy system, but because you must check with the legacy system each time, you don't realize the speed benefit.

If you are willing to accept a small possibility of discrepancies, you can remove the speed blockage caused by the legacy system. You could run a synchronization program that verified that a particular customer was in both databases, and removed any customers from the other database when they disappear from the legacy database. For a large database, this may be unfeasible.

Some legacy systems keep a transaction log, so you could run a synchronization program that looked for deletions in the transaction log and perform the deletion on the other database. This is a very reasonable solution. You don't have to perform a large number of queries to synchronize the database.

If you are only accessing the databases through the encapsulation, this problem doesn't occur, because the encapsulation knows whenever data is deleted. You may still have some problems if one database is down. You may need a two-phase commit protocol.

Using a Two-Phase Commit Protocol

A two-phase commit protocol allows you to make sure an operation can be performed, and then perform it. The idea is that when you have an operation that spans multiple databases, you tell each database, "This is what I want to do, are you ready to do it?" Each database gets ready to perform the operation, or replies that it can't. This is the first phase.

The second phase occurs when all of the databases have responded that they can perform the operation (most of them have actually performed the operation, but not committed it as permanent). You now tell the databases to commit the operation. If one of them fails at this point, you assume that it will be taken care of by the database. In other words, it is the database's responsibility to complete the transaction once it has said that it can.

If, during the first phase, any database refuses to perform the operation, you tell all the databases to back out of the operation. Again, it is the responsibility of the databases to undo whatever changes have been made.

If you have a system that supports the two-phase commit protocol, chances are you can also find a good TP monitor that will supervise the two-phase commit, freeing you from having to write it. If you use a TP monitor, you can always use the native method mechanism to create a Java interface to the TP monitor.

Implementing a Two-Phase Commit

When your system doesn't support a two-phase commit, you can either perform a single-phase transaction and log any errors that come up, or you can implement your own two-phase commit. Unless you're anticipating a large number of failures, you may be better off just logging any problems.

Writing your own two-phase commit protocol is decidedly non-trivial. You should have a firm grasp of database techniques before you attempt this.

When a database system performs a two-phase commit, it writes the transaction to a log, then performs the transaction without committing it. If the transaction is aborted, it undoes the changes. The log is used in case the database fails during the transaction. Once the database has responded in the first phase, it must be able to either commit or abort the transaction.

Generally, you implement a two-phase commit by saving enough information to be able to restore things to the way they were before the transaction, then performing the transaction. If the transaction is aborted, you put things back the way they were.

If you have a locking mechanism on the legacy system, you should use it while waiting for a commit or abort in the second phase. If you have performed a change in the first phase and another transaction is able to come in and use the changed version of the data, you may not be able to back out of the change. If you are able to lock the data against changes, this can't happen.

Some Real-World Examples

If you've never been through a legacy system migration before, you are probably overwhelmed by the number of options and the lack of certainty with which they have been presented. Legacy system migration is often a difficult struggle. The best thing you can do is to approach it with a set of strategies and an open mind, and see what comes up.

An Example Legacy System

Suppose you are working with an airline, with a cluster of mainframes, a wide-area network, and thousands of old terminals spread all over the world. The terminals are connected to the mainframe via the wide-area network. Figure 32.12 illustrates this configuration.

Figure 32.12: Many legacy systems have a network, one or more mainframes, and a group of terminals.

The following examples are adapted from actual running systems. Your systems may be radically different, but the steps you take are roughly the same.

Since you are probably not familiar with the kind of system normally found at an airline, here are thumbnail sketches of the mainframe, the WAN, and the terminals.

Creating a New Application for the Existing Terminal Base

Suppose you want to create a company-wide e-mail system. You are faced with a number of choices:

Assume that you want to save the company millions of dollars and create a modern
e-mail system while encapsulating the legacy terminals.

In keeping with the philosophy of not letting the legacy system drag down your design, you design an e-mail system that allows you to send full ASCII text messages, as well as attach binary files to the messages.

Hopefully, you considered buying such a product off-the-shelf. There is far too much custom software that needs to be written for you to spend your time writing things that have already been done a million times.

Armed with your new design, you try to determine how to fit the legacy terminals into the plan. Normally, you have only the mainframe and the terminals to deal with. In this case, however, you have a third element-the network. You could place an encapsulation in two different places. Figure 32.13 shows a scheme where you encapsulate the mail system by making it look like the wide-area network to the terminals. The terminals send information to your encapsulation thinking it is the wide-area network.

Figure 32.13: You can encapsulate the mail system by emulating the netwrok.

Your other option is to make your mail application hook into the network and behave like the mainframe. Figure 32.14 illustrates this configuration.

Figure 32.14: You can also encapsulate the mail system by emulating a mainframe.

There is a third option, which is using the mainframe as the front-end for the application, and passing data from the mainframe to the application. In this particular solution, however, you want to avoid writing code on the mainframe.

Each choice brings its own problems. If you try to emulate the wide-area network, you will probably experience some headaches because you'll have to install code wherever the network connects to the terminals. In general, you want to hook in at as few places as possible, preferably one.

If you try to emulate the mainframe, you want to avoid having to set up your own IBM channel and performing the channel protocol. If you get desperate, you can operate that way. In this case, however, there is a better option.

Recall that the network is highly programmable and supports X.25. Also remember that you may often reap huge benefits by adding a little code to the legacy system to open it up to your encapsulation. By adding some code to the network, you can use X.25 to emulate a mainframe rather than using the channel protocol.

The notion of emulating a legacy host is like a reverse screen-scraping. Rather than pulling data off screens generated by a legacy system, you're generating the screens.

Figure 32.15 illustrates this configuration.

Figure 32.15: A little interface code in the network makes for a simple encapsulation.

Now that you have a way to emulate a host via X.25, you must decide how you will use the X.25 protocol. You will need to use a native method call of some sort, or, as an alternative, you can create a program in C or C++ that accesses the X.25 line and communicates with users of the X.25 line via TCP/IP. In other words, you create an X.25 server.

Figure 32.16 shows how a Java program could access an X.25 network via this X.25 server.

Figure 32.16: A Java program uses a TCP/IP server to access the X.25 network.

This technique of creating small servers to handle specific interfaces or devices is a reasonable choice for data streams. This is much more efficient than CORBA, since you are just passing large blocks of data without interpreting it. CORBA is much better for message-based communications and remote method invocations.

At this point, you have the connectivity problem solved. You can communicate with the network from your Java program, and you will be able to exchange whatever information you need in order to emulate a mainframe.

Now, you actually implement the encapsulation. The job of the encapsulation is to take a command from a legacy terminal and turn it into a method invocation in the e-mail system, then gather the results and display them in a form that the terminal will understand.

The encapsulation acts like a client to the e-mail system and like a server to the legacy terminal population. Figure 32.17 shows the e-mail system with PC-based clients and legacy terminal clients.

Figure 32.17: The e-mail system interacts with both legacy terminals and PC-based terminals.

As you can see, the e-mail system is not limited by the constraints of the legacy terminal population, but it is still able to service those terminals. Your e-mail system is able to work in the world of distributed clients, where its users may be reading their mail from hand-held terminals and other devices.

Creating a New Interface for an Existing Application

Suppose you wanted to create a new interface for a legacy application. In this case, assume that you are trying to put a pretty front-end on an airline reservation system. Your first task, before you even think about connectivity, is designing your reservation system.

Remember, your system doesn't have to work exactly the same way that the existing system does. You'll probably want to simplify the interface and require as little typing as possible.

This is especially important if your goal is to allow agents at the airport to walk around with small hand-held terminals and provide assistance to passengers from outside the confines of a desk. This allows for more personalized service, and this is where you are really going to realize the benefits of Java.

Again, for the sake of simplicity, assume that you have come up with a good, user-friendly design. You aren't ready to move the airline's reservation off the mainframe, however. So, if anyone is going to use your new application, you're going to have to encapsulate the existing reservations system and make it look like your new application.

Now it's time to look at connectivity. Again, you have the mainframe, the network, and the legacy terminals. Start with a simple screen-scraping and proceed from there. In other words, the encapsulation is going to emulate a terminal. You again have choices for where you put your encapsulation:

The X.25 interface looks like the best candidate here because it's cheap and non-proprietary. Plus, you could use the same X.25 server from the e-mail system to access X.25 from Java.

Figure 32.18 shows the relationship between the encapsulation, the X.25 server, and the mainframe.

Figure 32.18: The network's X.25 interface permits easy emulation of a legacy terminal.

In a screen-scraping setup, your encapsulation logs on to the host system with the same commands that a user would normally type. If you are screen-scraping an IBM 3270 application, there's good news and bad news. The bad news is that 3270 screens are no fun to emulate. The good news is that there are some libraries to help you. In the airline example, however, there are no libraries to assist you. After all, it's not like there would be a big customer base for such a library.

You must figure out what commands you would have to enter on the legacy system to perform each task that your new application needs to perform. This may involve single commands to the legacy system, or it may involve a whole series of commands.

If at all possible, try to minimize the number of extra commands your library has to perform each time. For instance, you don't want it to go through a complete login sequence every time it needs to do commands. Do the minimum number of commands that will ensure that your terminal is set up the way you expect.

Once you have your screen-scraping code written, the other side of your encapsulation acts like the server for your new application. In other words, to the regular clients, it looks like the new system. To the legacy system, it looks like a terminal or a group of terminals. Figure 32.19 shows this screen-scraping encapsulation.

Figure 32.19: A screen-scraping encapsulation is sometimes your only choice.

The screen-scraping method of encapsulation is often useful when adding new code to the legacy system is impossible, or cost-prohibitive. Screen-scraping is well-suited for rapid prototyping, however. You can throw a functioning model together fairly quickly, without having to add code to the legacy system.

As a mechanism for a production system, however, screen-scraping has some serious drawbacks:

The nice thing here, however, is that once you have a functioning system, you can work on improving it. If you can get raw data out of the legacy system, you will be much happier. The trick with some systems, like the airline reservation system, is that it wasn't built to give out data in a raw form, so you must add custom code.

When you want to receive raw data from the legacy system, you must decide how the legacy system should deliver the data. The channel interface is a very fast interface, but it takes more work. If you are in a hurry, you may be better off finding an easier solution.

This reluctance to directly hook up to an IBM channel is specific to the airline reservation system. If you are connecting to a regular SNA host, this is a very viable solution, and there are many software/hardware packages available that do this.

You also have the option of making some kind of binary data stream to a terminal, which would most likely require a lot of cooperation from both the legacy system and the network.

The network makes a nice option here. With a little code on the reservation system and on the network, you can get data over the X.25 interface. The legacy system passes data to the network over the channel, and the network hands it to you via the X.25 interface.

Yet again, you have the X.25 server program to keep your Java encapsulation from needing any native methods to access X.25. Unlike screen-scraping, you don't have to perform a series of commands. You can work out a set of messages to be passed between the reservation system and your encapsulation. You won't have to go through the pain of extracting information from text output stream. Instead, the information you need will be laid out in specific places in the messages sent from the reservation system.

Figure 32.20 shows how a Java encapsulation program can use X.25 to exchange binary data with the legacy host.

Figure 32.20: Your encapsulation can exchange binary data with the legacy system.

Assuming you have the time and the resources, a direct channel connection to the mainframe is the best option in terms of bandwidth. When you need such an interface, you may be better off finding a firm that specializes in channel communications and getting them to write the channel interface code.

Assuming you have a way to speak your system's channel code, you should create something similar to the X.25 server. It should speak channel protocol on one side and TCP/IP on the other. Again, this means that you can connect a Java application to the channel gateway with a simple socket instead of creating non-portable native methods.

As with the X.25 solution, when passing information over the channel, you need to figure out what data your encapsulation needs to send and receive, and then add code on the legacy system to handle the requests. Figure 32.21 shows a channel-based encapsulation configuration.

Figure 32.21: A direct channel connection to a mainframe gives you high-speed access to data.

Clearing a Path for Migration off the Legacy System

When you encapsulate part of a system in order to use newer technology, you gain more than just the use of the newer technology. You gain a path for migrating off the legacy system.

This whole use of encapsulation boils down to one thing-changing a system in pieces rather than tackling the whole thing at once.

Encapsulation helps insulate you from "flash cuts" where everyone suddenly switches over to a new system. While there are cases when these cuts work, many times you end up switching back and forth while all the bugs are worked out. You'll still have these situations where you go back and forth, but you don't have to switch everything at once.

A typical migration path for upgrading both the legacy system and the legacy terminals usually requires at least one encapsulation. The kind of encapsulation you use depends on which part you want to change first.

No matter which part of your system you want to change first, the first step in migrating away from the legacy system is always the same. You have to figure out what the new system is going to look like. Until you do that, you can't change a thing.

When you decide to migrate your terminal population first, you must create an encapsulation for the legacy system. You encapsulate the part you aren't changing. As shown in Figure 32.22, you first encapsulate the legacy system, creating what looks like a server for your new application design.

Figure 32.22: When migrating legacy terminals, you first encapsulate the legacy system.

Next, you begin to migrate your terminal population over to the new application design. Since the old terminals and the new terminals are still accessing the same legacy system, you don't run into any coordination problems. Figure 32.23 shows how your terminal population might look halfway through the migration.

Figure 32.23: New workstations use the encapsulation to access the legacy system.

Finally, when all the terminals have been migrated over to the new application design, you can create a new server to replace the legacy system, as shown in Figure 32.24.

Figure 32.24: Once the legacy terminals are gone, you can replace the legacy system.

When you migrate the legacy system first, you encapsulate the terminal population first. In other words, you create an encapsulation that translates commands from the legacy terminals into commands understood by the new application design. Figure 32.25 illustrates this encapsulation.

Figure 32.25: A terminal encapsulation allows legacy terminals to access the new system.

Once the new system is in place, you can migrate your terminals over to the newer terminals. Again, since the new terminals access the same system as the old system, you don't have any coordination problems-there is only one system running. Figure 32.26 illustrates this migration in the process of changing the terminals over.

Figure 32.26: Legacy terminals and new workstations access the new system.

Finally, when you have migrated the last terminal over to the new application design, you are done.

Legacy system is a lot harder than it sounds. The techniques presented in this chapter represent your battle strategy. You use them to attack each new legacy system in a different way. Even though you have a good strategy, you still have to fight the battle-or write the code, in this case. That's where the real fun begins. Each system has its own challenges. You may not enjoy it while you're doing it, but sometime down the road, you'll enjoy recounting all the peculiarities of each system you worked with. They all have their own personalities, brought about by years of tweaks and changes.

Legacy system migration is crucial to bringing more companies onto the Internet and really advancing into the information age. Very few companies want to create a new system from scratch just so they can be on the Net.