The first tutorial concentrates on development of a simple application to quickly take you through the entire development process. In it you will learn how simple it is to use Java, along with four basic classes in the Managed Object Framework (MOFW), a set of interfaces at the heart of the Bojangles programming model. MOFW interfaces are usually paired around an essential application development task, with one interface for the "client" and one for the "server". After introducing Java, this tutorial will focus on Manageable and EDStream interfaces that enable generic bootstrapping (i.e instantiation or activatation) of "root" objects anchoring the application. Next, the Managed and BaseCollection interfaces are introduced; these interfaces enable entire collections of objects to be managed.
The second tutorial builds upon the first, introducing additional MOFW interfaces to enable development of entire frameworks of objects.
This tutorial will take you through capturing some business logic of a domain into a Java object, then subclass it to make it generically Manageable by a client. The resulting component will be extended so that it is Managed within a BaseCollection that provides various services like identity, persistence and transactions. Distribution is added in the final step, so that business objects can be used by remote client applications. It is added following a similar pattern to that making a Managed object.
There will be a separate subsection for each of the four steps enumerated above followed by a section to summarize the key concepts.
package COM.ibm.jaws.tutorial1.example1; public class Account { protected float balance = 0; public Account(float initialBalance) { balance = initialBalance; } public void deposit(float amount) { balance += amount; } public void withdrawal(float amount) { balance -= amount; } public float getBalance() { return balance; } }
The Account BO as shown is rather simple by design. Later versions of this implementation (in the subsequent tutorials) will check for and handle negative balances on the withdrawal method to illustrate some of the advanced features MOFW like relationships and exceptions.
For this example, we offer an application that tests all the methods of the Account BO above:
import COM.ibm.jaws.tutorial1.example1.Account; public class AccountTest { public static void main(String args[]) { // Get the checking, savings and transfer amount from the command // line arguments float cAmount = Float.valueOf(args[0]).floatValue(); float sAmount = Float.valueOf(args[1]).floatValue(); float tAmount = Float.valueOf(args[2]).floatValue(); // Initialize the accounts from the constructor Account checking = new Account(cAmount); Account savings = new Account(sAmount); // Make the transfer checking.withdrawal(tAmount); savings.deposit(tAmount); // Print the results (use subroutine for modularity) printBalance("Checking", checking); printBalance("Savings", savings); } public static void printBalance(String name, Account account) { System.out.println( name + " account Balance = " + account.getBalance() ); } }
As the code shows, AccountTest initializes two accounts from balances entered on the command line, transfers a third amount specified from the "savings" to the "checking", and then displays the resulting balances with verification that the proper transfer took place.
The two classes (Account and AccountTest can be compiled using the Java compiler and executed with the following result:
javac Account.java javac AccountTest.java java AccountTest 10 20 5 Checking account balance = 5 Savings account balance = 25
One value add of Java illustrated by this example is that there was no need to use a complicated makefile or build a Dynamic Load Library (DLL) in order use the Account BO class within the application.
Further, the Web browser and Java interpreter work together to download the various applet and business object classes to the client as needed, making Java based applications rather simple to administer.
The primary difference between an applet and an application is in how the applet is invoked (via the init() rather than main() method) and how the applet gets its parameters and displays its results (via special Java supplied library objects). The following code shows a "conversion" of the application shown above into an applet.
Once the applet has been defined and "installed" into the appropriate directory, it can be invoked via HTML like the following:
<.html>. <.head>. <.title>.SWGTC ATM Applet Demonstration<./title>. <./head>. <.body>. <.center>. <.h3>.Applet (Automated Teller Machine)<./h3>. <.applet name="ATMApplet" code="COM.ibm.jaws.tutorial1.example1.ATMApplet" codebase="/" width=500 height=250>. <./applet>. <./center>. <./body>. <./html>.
By "browsing" the Web page with a Java enabled browser, the applet can be executed with a GUI rather than via a "batch" command line process.
This section of the tutorial shows how the Bojangles programming model enables basic object services to be added without affecting the business logic in any way by transforming the BO into a Manageable object. A Manageable object is one that can enables the client application to initialize the object (via internalizeFromStream()), and export its essential data (through the externalizeToStream() method). Since the client can choose the implementation of the InputEDStream from which the data is imported, or the OutputEDStream into which it is exported, services like persistence, caching, and recovery can be generically (and efficiently) enabled.
The steps to transforming a BO into a Manageable object, and coding applications to use them are straightforward:
This section will take you through each of the above topics one at a time.
In any event, separating the interface from the implementation is a relatively simple task that entails:
The following shows the Account class transformed into an interface:
package COM.ibm.jaws.tutorial1.example2; include COM.ibm.jaws.mofw.Manageable; public interface Account extends Manageable { public void deposit(float amount); public void withdrawal(float amount); public float getBalance(); }
Implementing the Manageable interface methods entail identifying the essential data of the object (i.e., internal variables that cannot be derived from other sources) that must be written and read as part of the Manageable method implementations. For the AccountImpl, the essential data is simply the balance variable, and (including the default constructor) results in the following "Manageable" version:
package COM.ibm.jaws.tutorial1.example2; public class AccountImpl extends COM.ibm.jaws.tutorial1.example1.Account implements Account { // Constructors added public AccountImpl(float initialBalance) { super(initialBalance); } public AccountImpl() { } // Manageable method implementations public void internalizeFromStream(InputEDStream source) { balance = source.readFloat(); } public void externalizeToStream(OutputEDStream sink) { sink.writeFloat(balance); } }
The following test case exercises both mechanisms. It also shows how an InputEDStream can be used to simplify various "parsing" tasks, like converting the string command line arguments into the forms expected by the logic of the application.
include COM.ibm.jaws.motk.StringArrayStream; include COM.ibm.jaws.tutorial1.example2.Account; include COM.ibm.jaws.tutorial1.example2.AccountImpl; include COM.ibm.jaws.motk.TransientStream; public class AccountTest { public static void main(String args[]) { // Get the arguments into a stream to simplify conversion StringArrayStream s = new StringArrayStream(args); // Activate checking account using the specific constructor // Note how the command line argument is directly converted Account checking = new AccountImpl(s.readFloat()); // Activate savings account using default constructor and stream // Note: the command line arguments in the stream are "parsed" // directly into the object during the internalize Account savings = new AccountImpl(); savings.internalizeFromStream(s); // Make the transfer float transfer = s.readFloat(); checking.withdrawal(transfer); savings.deposit(transfer); // Check and print the balances checkBalance("Checking", checking); checkBalance("Savings", savings); } public static void checkBalance(String name, Account account) { // Print balance float aBalance = account.getBalance(); System.out.println( name + " account Balance = " + aBalance ); // Check balance by externalizing to and reading from a stream TransientStream s = new TransientStream(); account.externalizeToStream(s); if (aBalance != s.readFloat()) { System.out.println("\tExternalize NOT OK!"); } } }
We will leave it as an exercise to the interested reader to modify the test application above to use one of the persistent InputEDStream implementations found in the Bojangles Managed Object Toolkit (MOTK). This extention will doubtless add logic to write the updated essential state using an appropriate OutputEDStream implementation.
For this purpose, MOFW relies on objects called BaseCollections. In general, the purpose of a BaseCollection is to "manage" objects (called Managed objects). It supports methods that allow Managed objects to be created, activated and queried as part of your method implementations and application programs. A beneficial side effect of accessing business objects through a BaseCollection is that it can add services including but not limited to those like identity, persistence, transactions, concurrency, trace, debug, profiling and load balancing.
Bojangles has tools that automate this process for the BaseCollection implementations that we have provided. A later tutorial topic will describe how custom object service implementations can take advantage of our tool set if desired.
The changes to the test case to exercise this logic are relatively simple. The main differences between it and the version testing the Manageable interface are: a) code to activate the BaseCollection chosen (FilepoBCImpl in this case), and b) code to first try and locate the checking and savings accounts (using resolve()) before creating them (using createFromCopy() or createFromData()). The complete example follows in whch we also added logic to "deposit" the amount from the command line into the accounts if they already exist (instead of using it to initialize them):
include COM.ibm.jaws.motk.StringArrayStream; include COM.ibm.jaws.tutorial1.example2.Account; include COM.ibm.jaws.tutorial1.example2.AccountImpl; include COM.ibm.jaws.motk.FilepoBCImpl; include COM.ibm.jaws.mofw.BaseCollection; public class AccountTest { public static void main(String args[]) { // Get the arguments into a stream to simplify conversion StringArrayStream s = new StringArrayStream(args); // Activate the BC holding the accounts BaseCollection accounts = new FilepoBCImpl( "accounts", "COM.ibm.jaws.model.AccountFilepoManaged" ); // Try to find the checking account in the collection first Account checking = (Account)accounts.resolve("Checking"); if (checking == null) { // Since the checking account didn't exist, create a new one // Note: this version exercises the create from copy // which initializes from a "Manageable" local copy checking = new AccountImpl(cAmount); checking = (Account) accounts.createFromCopy( "Checking", checking, null ); } else { // Since the checking account already exists, add to it checking.deposit(s.readFloat()); } // Try to find the savings account in the collection first Account savings = (Account) accounts.resolve("Savings"); if (savings == null) { // Since the savings account didn't exist, create a new one // Note: this version exercises the create from data // which takes the data directly from a stream savings = (Account) accounts.createFromData( "Savings", s, null ); } else { // Since the savings account already exists, add to it savings.deposit(s.readFloat()); } // Make the transfer float transfer = s.readFloat(); checking.withdrawal(transfer); savings.deposit(transfer); // Check and print the balances checkBalance("Checking", checking); checkBalance("Savings", savings); } public static void checkBalance(String name, Account account) { // Print balance float aBalance = account.getBalance(); System.out.println( name + " account Balance = " + aBalance ); } }
This example used both versions of the create on the BaseCollection. The savings account was created (if necessary) from directly from data in a stream, while the checking account was created (also only when not found) from a "local" copy. It is important to realize that the latter method is merely a convienient form of the former, since the AccountImpl in this case is only Manageable, and is used to initialize the state of the Managed object created. Any client code using the createFromCopy() programming model should not mistake the local copy for the Managed reference (the same Manageable "copy" can be used to create more than one Managed if desired).
Because the accounts BaseCollection implementation provides persistence, if you run this test case again it will pick up exactly where the previous left off, even if the machine was shut down in the meantime. You can use the following example to "prove" this:
java AccountTest 10 20 5 Checking account balance = 5 Savings account balance = 25 java AccountTest 5 0 10 Checking account balance = 0 Savings account balance = 35 java AccountTest 100 0 0 Checking account balance = 100 Savings account balance = 35
Since a FilepoBCImpl was implemented as a Manageable object, we could have activated it via streaming, as we did for the Account objects in the previous example. We leave the specifics of this exercise to the interested reader; however, we wanted to reinforce the idea that an application will need to use some explicit activation mechanism to bootstrap various "root" objects. We will build on this concept to add distribution to the application, but at this point we have the ability to develop fully functional batch applications where root BaseCollections manage the various business object instances.
For example, a pure delegating proxy (also called a Stub) only depends on the communication service code being available in the client and an implementation of the server object's interface that handles the delegation (i.e. no real business logic on the client). The number of active proxies has only a minimal effect on the memory footprint (since only the identifier of the server object is maintained locally). Further, since all methods are forwarded onto the server for processing, there is no need to reconcile updates between multiple clients. The primary disadvantage is that every message entails a round trip communication with the server (with minor exceptions in some implementations, like getIdentifier() and isIdentical()).
At the other extreme, a fully replicated proxy requires that the class that implements the business logic is available to the client as well as the state of the business object for each "replica." However, once the replica is downloaded to the client, it may be able to operate in a disconnected mode, decreasing its dependence on a connection to the server object(s) in question. Depending on the implementation, reconciliation may be handled through any number of schemes, such as a locking and notification mechanism (i.e. the Model-View-Controller pattern).
For this tutorial, we will choose a service implementation that relies on lightweight Stubs (specific to the business object interface) that forward messages through an IIOP based communication service to a Skeleton (specific to the business object implementation) on the server that translates the message back into a Java method invocation.
Our example uses the Netscape server to start up a serverlet (we have dubbed our version the Servlet) to minimize the system management overhead associated with the application (with insuring that the server process is up prior to making a connection and sending a message).
Of course, the details of developing the server will vary depending on the implementation chosen, but they will be similar in that the code will need to: a) extend a specific implementation class (like ServerApplet), b) override a well known method that the framework uses to start execution (e.g. run()), c) activate a "root" BaseCollection within that method used to manage objects in the server (we reuse FilepoBCImpl as in the previous example), and d) connect to the distribution mechanism (the ORB in this case). The following shows the complete example:
public class AccountsServlet extends ServerApplet { static BaseCollection accounts = null; public void run() throws Exception { // Activate the BC holding the accounts as we did in the previous // test case if (accounts == null) { accounts = new FilepoBCImpl( "accounts", "COM.ibm.jaws.model.AccountFilepoManaged" ); } // Create the server orb with the accounts BC and socket // obtained from the Servlet ORB orb = (ORB) new ServerORBlet( accounts, null, getClientSocket() ); // Now "run" the ORB which reads messages from the // wire (using IIOP) and dispatches them to the activated // objects using generated "skeletons". This method // executes in an "infinite" loop. orb.run(); } }
This code could have been even simpler, since the only logic "of consequence" (i.e. that code that cannot be factored out by the implementation class being extended) is the code that activates the BaseCollection. We leave this simplification as an exercise to the interested reader (one possible answer), and now turn to the client side of the distribution equation.
// Logic deleted // Locate the "root" BaseCollection in the servlet named by the // URLToObjectContext parameter. orb = new ClientORBlet(this); BaseCollection accounts = (BaseCollection)orb.URLToObject( getParameter("URLToObjectContext") + ";/?COM.ibm.jaws.mofw.BaseCollection" ); // Activate the accounts using "resolve" as before checking = (Account) accounts.resolve("Checking");
In a sense, the ORB is acting like a BaseCollection of servers, using the server name passed in to locate the object(s) desired on the server (in this case the "root" BaseCollection). Interested readers may want to code up a version of the ClientORBlet that implements the BaseCollection interface to prove this. At any rate, now we have the ability to develop all kinds of different application styles, from standalone to fully distributed peer-to-peer.
In a similar vein, the "business logic" of the client application was able to be coded in such a manner that insulated it from the changes involved in moving from phase to phase. Review the test case code and you will see that the primary differences revolve around a) which objects serve as the "root" (the Account, AccountImpl, FilepoBCImpl, and ClientORBlet), and b) how these root objects are activated (via a specialized class method, a specialized constructor or a default constructor and streaming).
Although we did not provide explicit examples for this, the separation of implementation from interface will provide another "layer of insulation" from change as the application is maintained over time. If only the implementation of the business logic should change without affecting the interface, then none of the objects "downstream" will need to be regenerated. If a change to the implementation should affect only the essential data of the object, then the changes are limited to the associated Manageable class that implements the streaming methods. Of course this may mean that the Managed classes must be regenerated (especially if they are "mapped"), and any "legacy" data be converted, depending on the BaseCollection implementation chosen.
A final point that we want to stress is that at each step of the way, we have a completely functional object that is useful in its own right. Of course, the number of applications will increase as additional functionality is added. For example the pure java Account is useful in completely customized applications or those that can get by with purely transient objects. The Manageable AccountImpl can be used in a standalone application with streaming used as a generic explicit persistence mechanism, usually limited to a few "root" objects in the application. The Managed AccountFilepoManaged and associated FilepoBCImpl provide an implict persistence mechanism for managing large numbers of accounts suitable for a batch or command line application (and serving as the foundation upon which distribution can be added). Lastly, the AccountSkeleton and associated ORB enables the objects to be used remotely by any IIOP compliant client, while the AccountStub and associated ClientORBlet provided such a client suitable for use in a Java applet environment.
Of course, this "application" only involved a single business object class. The next tutorial will build on this example by expanding the "model" to include relationships between the Account, Customer and Bank objects. This example will be sufficient to illustrate how entire frameworks of objects will be implemented within Bojangles, and serve to complete the overview of the MOFW components.
Since both objects are modified during the course of this part of the tutorial, we will illustrate ways to limit the effect of changes (with respect to re-generating code and re-compiling it).
As an aside, we will show how a NamedCollection can be used by a BO wishing to provide "dynamic" attributes.