The Server

The wizard architecture discussed in Wizard Architecture is separated between a server and a client. The server has the responsibility of actually carrying out the purpose of the wizard. The responsibilities of the server include:

This section discusses each of these responsibilities in some detail.

Data storage and retrieval are accomplished by the WizardState. The WizardState stores data in the form of objects in a hash table. This allows the storage of any number of objects. The objects that are stored must be serializable. The WizardState has two methods for data storage and retrieval: setData() and getData(). These methods can be called either from the server or from the client. When the WizardState is instantiated from the wizard archive, it reads its initial data from the archive. The archive builder is responsible for writing the initial WizardState data into the archive. This will be discussed in detail in Native Support for Wizards.

The responsibility of retrieving data from the wizard archive is shared between the WizardState and the ArchiveReader. The ArchiveReader actually opens the wizard archive file and reads the data as either an input stream or a byte array. The WizardState is used by objects both within the server and within the client to request the services of the ArchiveReader. The WizardState implements the Mediator design pattern, which avoids proliferation of object references and interconnections throughout the system. This avoidance of object interconnections is an important aspect of the design that enables wizards to operate with a network connection between the server process and the client process.

Resolution of classes from the wizard archive is achieved by the ArchiveClassLoader in conjunction with the ArchiveReader. All of the resources neccessary to execute a wizard are contained within the wizard archive file. These resources include all of the class files the wizard will instantiate, except for those classes found in the core Java package.

When the ArchiveClassLoader is instantiated, it instantiates an ArchiveReader. The ArchiveReader finds the section within the wizard archive that contains the class bytes and submits them to the ArchiveClassLoader.

Before each class is submitted to the class loader, the ArchiveClassLoader determines if the class has already been submitted. Each class is accepted by the ArchiveClassLoader only once.

In a wizard hierarchy, there is more than one wizard archive, and an ArchiveReader for each wizard archive, but only one ArchiveClassLoader. The separation of responsibilities between these two classes makes the wizard hierarchy possible.

The execution of tasks is done through the use of the Sequence class and subclasses of Task. A Sequence is a container for one or more Task objects. The Task object is the mechanism through which the wizard can manipulate the system on which the wizard server is executing. The Sequence object is given a set of Task objects through calls to the addTask method. The Sequence object maintains the order in which the Tasks were added.

The Sequence is executed through a call to the WizardState. A Sequence can be performed or reversed. The Sequence.perform method performs each Task within the Sequence in order. The Sequenc.reverse method reverses each Task in the Sequence in reverse order. This allows the wizard to do a specific action, and then undo that action at a later time. If the Sequence is canceled (through a call to the Sequence.cancel method) during the perform method or the reverse method, the Sequence sends a cancel message to the Task that is currently executing.

The Sequence is able to send progress messages to the client through the WizardState. In this way, a Sequence requiring lengthy execution can keep the user informed that progress is being made. The Sequence always reports a progress between 0 and 100.

When the Sequence has finished its execution, the WizardState can invoke a callback method that is passed as an argument to either the performSequence method or the reverseSequence method. The callback is useful, because it indicates when the Sequence has completed, and allows the object that initiated the Sequence to perform checks to determine the outcome. For example, if one of the Tasks in the Sequence failed, it might put a flag into the WizardState through a call to the setData method to indicate what type of failure occurred. The callback method can be used to get this information and display a warning to the user.

Each Task in the Sequence can set its progress into the Sequence object. The Sequence object calculates the overall progress for the entire sequence each time its setProgress method is called from a task. The Sequence calculates the overall progress based on the estimated time each task in the Sequence will take. Each Task subclass should override the estimatedCompletionTime method to indicate the approximate completion time of the Task, measured in milliseconds.

If the progress is going to be displayed with a progress bar on a client panel, make the progress bar move as smoothly as possible. For example, if a Task that takes a long time to perform only sets the progress at the beginning and end of the task execution, the progress bar movement will be erratic.

The wizard archive contains all of the information required to instantiate the wizard. The WizardState facilitates the creation of the wizard archive by writing the information required to instantiate the server classes to an output stream. This mechanism can be broken by setting non-serializable objects into the WizardState. It is important that all of the objects set into the WizardState through calls to setData implement the Serializable interface. Task subclasses must also implement the Serializable interface.

Any reference to the WizardState object within a Task should use the "transient" modifier so the WizardState is not serialized with the object. The WizardState object does not implement the Serializable interface to make it obvious if such a mistake is made. This sort of mistake would be caught by running the archive builder, which would exit with an exception.

The initialization of the wizard is important to understand. When the wizard archive is executed, the ArchiveClassLoader is the first object to be instantiated. An instance of ArchiveReader is created, which initializes by reading the class bytes section of the archive. The ArchiveReader sends the classname of each class found in the archive to the ArchiveClassLoader to determine if the class bytes for that class should be submitted. If the class is not already known to the ArchiveClassLoader, it will accept the class bytes.

The ArchiveReader then instantiates a WizardState object. During initialization of the WizardState object, all of the data objects that were set into the WizardState at buildtime are read from the archive. After the data is recreated, the Sequences and their Tasks are read from the wizard archive.

If the WizardState has children, an ArchiveReader is created for each child. Each ArchiveReader object then tries to submit its archive classes to the ArchiveClassLoader. The ArchiveReader creates a WizardState object for each child. This creation and initialization of the WizardState and its children continues recursively until there are no more WizardState children. There is no theoretical limit to the number of child WizardState objects that can be created. The practical limit is dependent on the amount of memory in the system using the wizard hierarchy.

When all of the child WizardState objects have been created, the root WizardState object performs the initialization sequence. The wizard archive builder can put tasks into the initialization sequence that should be performed when the wizard archive is launched.


Last modified: Fri Nov 25 16:48 PDT