This tutorial covers the basic ideas behind custom install wizards, and how to write custom panels that interact with custom ServerObjects and custom Tasks to accomplish things. It is assumed that you have a basic understanding of Web Start Wizards, and their purpose.
A custom install wizard is basic install wizard, with custom panels and/or tasks that have been added to perform installation of a product that requires customization.
The Web Start Install Wizard API can be used to create self-contained wizards that install a product, or a suite of products. The basic install wizard has a few common things, such as directory selection, install progress, and a summary of what installed. Beyond that, each wizard can be customized to do a custom installation, accomplishing tasks specific to that product. This tutorial covers creating install wizards which are customized for a particular product. If your product is simple, you can simply use the provided sequence of panels and actions, without any customization. You would extends the InstallArchiveWriter API. If your product contains customizable installation options, read on!
A custom install wizard can perform custom installations that a standard install wizard would otherwise not do. For example, suppose your product is a web server. After installation, you would like to ask the user which port the newly-installed server should listen on. Then, you want to save this information in a file so that the newly-installed product can use the information when the user runs it for the first time. In this case, you would create one Panel that prompts the user, and an associated ServerObject that would write the file (these concepts are explained below).
In order to create a custom panel, your panel must inherit from
WizardLeaf, the common superclass for all wizard panels.
Your panel must also override certain methods to draw its GUI,
and interact with the rest of the system (much as a
java.awt.Panel might override paint() to draw
itself).
The "interaction" metioned above involves your panel interacting with ServerObjects and Tasks to perform their tasks. For example, if your custom panel reads in a file and presents it to the user, you would write a ServerObject to read in the file, and have your panel communicate with the ServerObject to read the file and present it to the user. Why is this necessary? Why not just read the file using java code written in the panel? The answer is that wizards were designed to be client/server. The user interaction takes place on the client, and then the client subsequently interacts with the server to actually perform tasks. The client and server are completely separate entities, even if they are executing on the same computer.
Currently, when a Web Start Wizard is executed, the client and server exist on the same computer that the wizard is run on. However, in the future, wizards might be "remote-enabled", meaning the client and server might not exist on the same computer. Rather, they will exist separately and communicate via the underlying network. It is strongly recommended that you keep your client code (i.e. the code in your panel class) separate from the server code (i.e. The code that interacts with the underlying system), so that in the future your wizard will run remotely, with no additional work.
The WizardLeaf class is the superclass of all custom panels that you may create. In other words, if you are creating a custom panel, make sure that your class extends WizardLeaf.
This class provides the interface for your panel to interact with the rest of the wizard. Your panel can then call the methods it inherits to accomplish certain things, such as querying the user, or performing a Sequence. Your panel can also override certain methods that will cause the wizard to behave differently. You should be familiar with the APIs of the WizardLeaf, as well as the WizardTreeManager API, and the WizardState, since these are the main APIs that your custom panel can use.
A basic panel can do the following:
createUI() method. This method is
called when a panel is encountered in the sequence of all
panels. This method is only called once, the first time the
panel is visted. Therefore, in this method, you should
create and initialize any GUI objects (such as TextAreas or
Buttons), and place them onto the panel (via the
add() method of java.awt.Panel, which
your panel inherits from).
beginDisplay() method. This
method is called each time your panel is visited. This
method should be used to refresh the GUI components, if needed.
It should not be used to create and add GUI components
to the panel, since the user may visit the panel many times.
For example, if you had these lines in your
beginDisplay() method:
Button b = new Button("Press Me"); add(b);
Then, every time the user visited the panel, a new button
would be added to the panel! If the user clicks "Back" and
"Next" several times, many, many buttons would appear, which
is probably not what you had intended. Put this code in the
createUI() method.
isDisplayComplete() method. This
method is called when the user clicks the "Next" button to
advance to the next panel after yours. In this method, you can
validate any data the user entered, to make sure the data is
valid. If the user entered good data, your method can
return true, indicating it is safe to move on. If
the user did not enter good data (e.g. They entered a number
instead of their name), then your method should detect this and
return false, indicating the wizard should not
advance to the next panel. You also might want to inform the
user of what they did wrong. One way to do this is to use the
displayQuery() API of WizardTreeManager.
Each panel has a reference to the Wizard Tree Manager (there is
only one manager, and it manages all panels.) To
reference the manager, use the getTreeManager()
method call from your panel. The example below includes this
idea.
addRuntimeResources() method.
This method is used to declare any classes that your custom
panel will need at runtime. Since the final wizard you create
is fully self-contained, any classes that you wrote that are
needed by the custom panel must be declared in this method so
that it gets included into the wizard, available at runtime.
Otherwise a ClassNotFoundException will result, and
your wizard will not function correctly.
CLASSPATH when building your wizard.
Tasks are grouped into Sequences, which allow multiple tasks to execute in a particular order, and to combine each task's progress into an overall progress that can be reported back to the panel that executed the Sequence.
The two main methods of the Task class that your custom task should override are:
perform(). The perform() method
is called when the sequence that contains it is performed. In
this method, your tasks should perform its primary function.
Tasks are performed in the same order as they were added to
the sequence.
reverse(). The reverse() method
is called when the sequence that contains it is reversed. In
this method, your tasks should reverse its primary function.
Note that this method is not required. Tasks are reversed in
the same order as they were added to the sequence.
perform() method,
you can also call setProgress to update the
sequence's idea of the overall progress of the sequence. You
should pass an int between 0 and 100 to indicate the
percentage of the task's progress is complete.
There are other uses of the Task class that can be used. Consult the API documentation for more advanced ideas.
perform() and reverse(). Your server
object can provide any API that you wish, and your panels
can call on that API, and return results. There are no methods to
override, it simply exists and can be called upon by panels
similar to the way sequences are called upon.
The ServerObject is useful for performing small, relatively fast tasks that do not need the underlying progress architecture inherent with Tasks.
In order for your custom Panels, Tasks, and ServerObjects to be available at runtime, you must write your builder to include them. In addition, your panel class definition must declare certain constructors in order to work with the underlying wizard architecture.
The example below creates a complete install wizard panel sequence as well as a Product Tree, which is a way to represent your product to be installed. This is the same sequence of panels that might be generated if one were to use the InstallArchiveWriter API, except the source code is provided for you to modify at will.
Notice the lines pertaining to the instantiation of the
wizard panel sequence (located in the
buildPanelTree() method). In this example builder,
We are building a
tree structure that represents the panel sequence. The
panels are then visited in a depth-first order
(i.e. left-to-right, children first).
Each panel is
instantiated and then added to its parent. The first node
instantiated is added as the root node, which is
visited first. Each node is either a WizardComposite
or a WizardComponent.
The difference is that WizardComposites can have children,
whereas WizardComponents cannot. In addition, some nodes
can dynamically modify the sequence of panels displayed.
For example, the SkipNode
can cause its child nodes and panels to be skipped if some
condition is met. When the SkipNode is visited, its
condition is evaluated, and the skip() method
returns true to indicate that this node, and
all of its children, should be skipped.
You will instantiate your custom panels and nodes, and build a tree in the same manner as the example below. Feel free to copy this builder and modify it for your own uses. Here is the source code.
CustomTask myTask = new CustomTask();
Sequence mySequence = new Sequence();
mySequence.addTask(myTask);
getWizardState().addSequence("MyCustomSequence", mySequence);
You can then perform your sequence from a panel by using:
WizardTreeManager manager = getTreeManager();
manager.callServerObjectMethod(
getRoute(),
"performSequence",
new String[] {"com.sun.wizards.core.Route.class",
"java.lang.String.class",
"java.lang.Boolean.TYPE",
"java.lang.String.class"},
new Object[] {getRoute(null),
"MyCustomSequence",
new Boolean(false),
"sequenceComplete"});
See the WizardTreeManager.callServerObjectMethod() API for more information on how to make this method call.Your sequence will then be executed, and either wait for completion, or return immediately, depending on the API you used to make the method call. In this example, the method call will not wait for completion of the sequence. When the sequence is complete, the
void sequenceComplete()
method will be called.
If the panel that calls your sequence has a method with the following signature:
public void setProgress(int[] progress)
{
[...]
}
Then, when one of the tasks in your sequence calls
setProgress(), the sequence is "bubbled up" and weighted
with all the tasks in the progress, and reported back to
your panel through this setProgress method in your
panel. The number reported is a percentage of the entire sequence, from
0 to 100, inclusive.
If the sequence has only one task, then the progress reported is for that
sole task.
CustomServerObject obj = new CustomServerObj();
getWizardState().addChildObject("MyObject", obj);
Then, to call upon your custom server object's Integer
doThis(Integer start) method from a panel, you would:
WizardTreeManager manager = getTreeManager();
Route serverObjectRoute = getRoute().getChildServerRoute("MyObject");
Integer rtnValue = (Integer)
manager.callServerObjectMethod(
serverObjectRoute.copy(),
"doThis",
new String[] {"java.lang.Integer.class"},
new Object[] {new Integer(5)});
The difference between ServerObjects and Tasks/Sequences are that
ServerObjects do not have the implicit progress reporting feature,
nor are they asynchronous (unless you implement it within
the ServerObject). However, the ServerObjects are not limited to
the API enforced by the Task/Sequence infrastructure.
These ideas are all used in the following example...
CustomPanel into the wizard sequence just before the WelcomePanel.
The Custom Panel asks the user for a filename. It then uses a
ServerObject to see if that file exists, and if it
does, runs a Sequence which contains a
Task which does nothing (remember, this is an
example). While the sequence runs, the progress is reported. When the
Sequence is done, the next panel is visited automatically.
CustomServerObject.java, and place it in our
classes subdirectory of the SDK. Here is the code for
it.
import java.io.*;
import java.util.*;
import com.sun.wizards.core.*;
/**
* The CustomServerObject is an object that is used to see if other files
* exist.
*/
public class CustomServerObject implements ServerObject, Serializable
{
public static final String SERVER_OBJECT_NAME = "CustomServerObject";
/**
* A runtime handle of the WizardState that we belong to
*/
private transient WizardState wizardState = null;
/**
* Create a new CustomServerObject
*/
public CustomServerObject()
{
}
/**
* This method sets the WizardState into the object at runtime
*/
public void setWizardState(WizardState wizardState)
{
this.wizardState = wizardState;
}
/**
* Get the runtime classes required by this ServerObject.
*/
public void addRuntimeResources(Vector resourceVector)
{
resourceVector.addElement(new String[] {null, "CustomServerObject"});
}
/**
* Sees if a file exists.
*
*/
public Boolean doesExist(String fileName)
{
if (fileName == null)
{
return new Boolean(false);
}
File file = new File(fileName);
return new Boolean(file.exists());
}
}
doesExist(String
fileName). This is the method that the panel sues to see if
the file exists on the server. The other methods and variables:
addRuntimeResources(): Declares which classes are
needed by this class. If you forget a class here, you will end up
with a ClassNotFoundException or a
NoClassDefFoundError.
setWizardState(): Fulfills the
ServerObject Interface. All server objects should cache
the runtime WizardState using this method. How you store it is up to
you. You can ignore it if you will never need it.
CustomServerObject(): Default constructor. You can
have any more you want. Remember, if you intend to serialize your
class, you should make sure and have a default constructor and
implement the Serializable interface.
SERVER_OBJECT_NAME: Convience string, references from
other classes when naming the object.
CustomTask.java, and place it in our classes
subdirectory of the SDK. Here is the code for
it.
import java.io.*;
import java.util.*;
import com.sun.wizards.core.*;
/**
* The generic task is a sample task that does
* nothing. The task is initialized with the
* amount of time the task should take. The
* task merely waits for the specified time.
*/
public class CustomTask extends Task implements Serializable
{
public static final String SEQUENCE_NAME = "CustomTask's Sequence";
/**
* The number of seconds this task takes to complete.
*/
private int completionTime = 0;
/**
* A flag indicating whether or not this task has been canceled.
*/
private transient boolean canceled = false;
/**
* Creates a CustomTask that waits the specified
* length of time, in seconds.
*
* @param completionTime The number of seconds this task
* takes to complete.
*/
public CustomTask(int completionTime)
{
this.completionTime = completionTime;
}
/**
* Perform this task. This method merely waits the amount
* of time specified in the constructor.
*/
public void perform()
{
/*
* This is the number of progress bar updates per second.
*/
int ticksPerSecond = 4;
/*
* Calculate the progress per update.
*/
double progressPerTick = ((double)100/((double)ticksPerSecond*(double)completionTime));
/*
* Update the progress bar tick times
* per second.
*/
for(int tick = 0; tick <= completionTime * ticksPerSecond;
tick++)
{
if(canceled)
{
return;
}
try
{
Thread.sleep(1000/ticksPerSecond);
}
catch(InterruptedException e)
{
}
setProgress((int)(progressPerTick * tick));
}
}
/**
* Cancel this task.
*/
public void cancel()
{
this.canceled = true;
}
/**
* Add the runtime class requirements to the specified vector.
* @param resourceVector The vector containing all runtime resources for this wizard.
*/
public void addRuntimeResources(Vector resourceVector)
{
resourceVector.addElement(new String[] {null, "CustomTask"});
}
}
SEQUENCE_NAME: Another convenience declaration, much
like SERVER_OBJECT__NAME above.
completionTime: So the Task will remember how long it
should execute for at runtime.
canceled: Used to know if the task gets cancelled so
it can stop what it's doing
CustomTask(int): Used
at buildtime to create a task and tell it how long it will execute at
runtime
perform(): The actual guts of the task. This method
waits for the number of seconds specified in the constructor, all the
while updating its progress (which gets sent back to the panel; see
below).
cancel(): Executed when the task gets cancelled (via
the cancelSequence() API). Do what you must here.
addRuntimeResource(): Same as above; declare any
needed classes here.
CustomPanel.java, and place it in our
classes subdirectory of the SDK. Here is the code for
it.
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import com.sun.wizards.*;
import com.sun.wizards.core.*;
/**
* CustomPanel asks the user for a filename
*/
public class CustomPanel extends WizardLeaf
{
/**
* Set to true once user gives good file
*/
private boolean passed = false;
/**
* Holds user answer
*/
private TextField file = null;
/**
* Shows prompts, and overall progress during Task execution.
*/
private Label label = null;
/**
* The prompt
*/
public static final String PROMPT = "Enter Filename:";
/**
* Creates a CustomPanel with no name.
*/
public CustomPanel()
{
}
/**
* Creates a CustomPanel with the specified name
* that presents the specified application for
* installation.
*/
public CustomPanel(WizardState wizardState, String name)
{
super(wizardState, name);
}
/**
* Creates a CustomPanel with the specified name, the specified
* route and wizard manager.
*/
public CustomPanel(String name, Route route, WizardTreeManager wizardManager)
{
super(name, route, wizardManager);
}
/**
* This method creates the user interface.
*/
public void createUI()
{
super.createUI();
file = new TextField(40);
label = new Label(PROMPT);
GridBagLayout gbl = new GridBagLayout();
Panel panel = new Panel(gbl);
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.insets = new Insets(20,10,20,10);
panel.add(label, gbc);
panel.add(file, gbc);
add(panel, "Center");
}
/**
* Called automatically during sequence's progress
*/
public void setProgress(int[] progress)
{
if ((progress != null) && (progress.length >= 1))
{
label.setText("Progress: "+progress[0]+"%");
}
}
/**
* Called when user presses "Next"
*/
public boolean isDisplayComplete()
{
WizardTreeManager manager = getTreeManager();
if (passed)
{
label.setText("Disabled");
file.setEnabled(false);
return true;
}
else
{
Route serverObjectRoute = getRoute().getChildServerRoute(CustomServerObject.SERVER_OBJECT_NAME);
/**
* Use server object
*/
Boolean exists = (Boolean)
manager.callServerObjectMethod(
serverObjectRoute.copy(),
"doesExist",
new String[] {"java.lang.String.class"},
new Object[] {file.getText()});
if (exists.booleanValue())
{
/**
* User Task/Sequence
*/
manager.setButtonEnabled("next", false);
manager.setButtonEnabled("back", false);
manager.callServerObjectMethod(
getRoute(),
"performSequence",
new String[] {"com.sun.wizards.core.Route.class",
"java.lang.String.class",
"java.lang.Boolean.TYPE",
"java.lang.String.class"},
new Object[] {getRoute(null), CustomTask.SEQUENCE_NAME,
new Boolean(false), "sequenceComplete"});
}
else
{
manager.displayQuery(
this,
"File does not exist. Cannot run CustomTask",
new String[] {"Dismiss"},
null);
}
return false;
}
}
/**
* Called when sequence is complete. We tell user and advance automatically.
*/
public void sequenceComplete()
{
WizardTreeManager manager = getTreeManager();
manager.displayQuery(
this,
"Sequence Complete",
new String[] {"Go To Next Panel"},
null);
passed = true;
manager.setButtonEnabled("next", true);
manager.setButtonEnabled("back", true);
manager.nextButtonPressed();
}
/**
* Get the runtime classes required by this panel.
*/
public void addRuntimeResources(Vector resourceVector)
{
super.addRuntimeResources(resourceVector);
resourceVector.addElement(new String[] {null, "CustomPanel"});
}
}
passed: Once the user gives a good filename and the
task is run, this flag tells the panel not to ask again, but to let
the user through.
file: Java AWT Component to hold the user's input
label: Java AWT Component to give the user messages
and to prompt him.
PROMPT: Just a prompt. No Black Magic here.
CustomPanel(): Default constructor. Good idea to
have one.
CustomPanel(WizardState, String): Buildtime
constructor that we will call in the builder.
CustomPanel(String, Route,
WizardTreeManager):Your custom panel MUST have one of
these! During Wizard initialization, all of the panels are
brought out of the archive and instantiated with this constructor. If
your panel does not have one, a NoSuchMethodException
will result, and your user will be very perplexed.
createUI(): Called once when the panel is
first shown. You should instantate your UI here. The custom panel
creates it's components here and adds them to the object itself.
setProgress(): Called if this panel executes a
sequence. The array passed in represents the progress if the
particular sequence it called, and any parent wizards (See the hierarchical wizard tutorial for
more information). Basically, with a single wizard, the array has one
element, the overall progress of the currently executing sequence. We
use this information to update the panel.
true, indicating to the wizard it is OK to move to the
next panel). Otherwise, contact the server object and call it's
doesExist() method, to see if the file actually exists.
If it does not, tell the user (via the displayQuery()
API), and refuse to go to the next panel (by returning
false). Otherwise, the file does indeed exist, so we run
the sequence by using the performSequence API.
Why do the two calls to doesExist() and
performSequence look so funny?. You may notice
that the two calls in the isDisplayComplete() method do
not look like ordinary java method calls. There are arrays of Strings,
arrays of Objects, and Routes involved in them. Why? The answer lies
in the client-server separation of wizards. Method calls from the
client to the server (like performSequence and
doesExist) might be travelling between two machines
connected by a network, instead of in the same computer. In this
case, the call must proceed through Java's Remote Method Invocation
architecture. we have abstracted this idea another level so that
panels have no idea they might not be on the same machine.
In addition, there is no dealing with any special Exceptions.
sequenceComplete(): This method is called by the
server when the sequence completes (since we declared this method to
be called at sequence completion (see the
performSequence() call above)). Here, we tell the user
the sequence is done, and enable the buttons we had previously
disabled, and automatically advance to the next panel.
addRuntimeResource(): Same as above; declare any
needed classes here.
com.sun.install.panels and the
com.sun.install.tasks packages in the SDK.
Most of the complexities of building an install wizard have been
abstracted in the InstallArchiveWriter
API. However, in this example, we will show a basic builder that does
not use the InstallArchiveWriter API. Rather, it
directly works with the ArchiveWriter API, just as the
InstallArchiveWriter class does. In this way, you can
see what kind of code is required to create a somewhat complex wizard.
Also, this can serve as a beginning cut at creating your wizard, with
further customization to be done by the developer as needed.
Here is the
code for the sample builder that generates an install wizard that
you might get out of InstallArchiveWriter. Feel free to
use this as the beginning of your customized install wizard if
desired.
The builder simply instantiates the client tree and the product tree and saves it in the wizard, to be used later by the Panel.
To compile the wizard (assuming you are in the directory where your custom classes are):
$ javac CustomTask.java CustomServerObject.java CustomPanel.java CustomBuilder.javaThis will result in all of the classes being built. Then, to run create the wizard archive itself:
$ java CustomBuilder
This will result in a file called myProduct.class, which represents your wizard.
$ java myProductThe first panel after the WelcomePanel will be our custom panel. Enter a filename. If the file exists, the task will run and reports its progress, and then you will be able to advance into the actual install wizard.
This concludes this Custom Wizard Tutorial. For more information on creating wizards for Solaris, please visit our website at www.sun.com/solaris/webstart/wizards/.