Swing-based application wizardry with the Wizard API If you’re faced with creating a Swing-based wizard from scratch, you’ll want to know about Tim Boudreau’s Wizard project. This installment of Jeff Friesen’s Open source Java projects series gets you started with the Wizard API and concludes with a hands-on installation wizard that is sure to please users and impress the boss. Editor’s note: This article has been updated with a new sidebar: WizardException fixed! Wizards (also known as assistants to Mac developers) guide users through complicated tasks such as installing software and setting up network connections. Although wizards are designed to be easy to use, creating them can be difficult. Fortunately for Java developers, NetBeans guru Tim Boudreau has created an open source project that takes the pain out of creating Swing-based wizards, by offering features such as An architecture that handles much of the work of implementing a wizard Automatic capture of standard Swing component values to a map during data entry — your wizard code can interrogate the map when it’s time for the wizard to do its thing Input validation Handling of lengthy wizard tasks on a background thread (with progress notification) so as not to disrupt the event-dispatching thread End-of-wizard summary panels Meta wizards In this installment of the Open source Java projects series, I’ll introduce you to Tim’s Wizard project. I’ll start by showing you how to obtain Wizard’s library and documentation, as well as introducing you to its examples. Next, I’ll take you on a tour of Wizard’s API, pointing out interesting features and things you should avoid. Finally, you’ll have the opportunity to create a MakeInstaller application, and see for yourself a realistic example of Wizard’s usefulness. Open source licenses Each of the Java projects covered in this series is subject to an open source license, which you should understand before integrating the project with your own projects. Wizard is subject to the Common Development and Distribution License. Get started with Wizard The Wizard project, which is hosted on Java.net, facilitates the task of creating Swing-based wizards via an API and implementation library. Visit the Wizard Project main page to download the library’s (ver. 0.992) wizard.jar distribution file. The library’s source code is available in the CVS. To begin familiarizing yourself with Wizard, follow the Quick Start Guide and Frequently Asked Questions links on the Wizard project page. You can also explore the online API documentation by following the Javadoc (online) link, or download the documentation’s WizardAPI.zip file by following the Download the Javadoc (zip file) link. NetBeans heritage Wizard originated as a replacement for NetBeans’ own Wizard API. The project has since evolved into a general-purpose API. Wizard examples Clicking Java WebStart Demo on the Wizard project page will lead you to a demonstration of a NetBeans tool, which uses Wizard to implement its own wizard. Figure 1 reveals this wizard’s dialog box, which presents the first step in the sequence of steps to complete. Figure 1. NetBeans Module Generator for Swing Components wizard. (Click to enlarge.) The dialog box’s left “sidebar” panel presents the wizard’s steps over a decorative image — the current step is bolded. The right panel presents the step’s page of GUI components. Navigation, Finish, and Cancel buttons appear along the bottom. In addition to the online demo, you can explore basic and advanced example wizards. You first need to build these examples, however. Complete the following steps to build the basic example: Create a wizardpagedemo directory below the current directory. Follow the Basic example source link to download the basic example’s AnimalTypePage.java, FinalPage.java, LocomotionPage.java, OtherAttributesPage.java, and WizardPageDemoMain.java source files into wizardpagedemo. Download wizard.jar into the current directory. Invoke the command below (which assumes a Windows XP platform) to compile the basic example’s source files: javac -cp wizard.jar;. wizardpagedemoWizardPageDemoMain.java After successfully compiling these source files, invoke the following command (which also assumes Windows XP) to run the basic example: java -cp wizard.jar;. wizardpagedemo.WizardPageDemoMain Both the basic and advanced examples present wizards that request animal information. Unlike the basic example, the advanced example demonstrates multiple wizards. API fundamentals Before you can use the Wizard library to create our own wizards, you need to understand its API. We’ll start with the fundamentals in this section, and move on to advanced topics in later sections. I’ll present excerpts from Wizard’s basic example to illustrate various API features. Packages and pages The Wizard API’s 17 classes and interfaces are organized into org.netbeans.api.wizard and org.netbeans.spi.wizard packages. The former package’s reference types are used to display wizards, which are implemented by the latter package’s reference types. A wizard manages pages, where each page describes one of the wizard’s steps and is an instance of the org.netbeans.spi.WizardPage class or a subclass. The following excerpt from the basic example’s AnimalTypePage.java source file defines a page via a subclass: public class AnimalTypePage extends WizardPage { // ... } The subclass is responsible for creating the page’s components, installing a layout manager, and adding the components to the WizardPage. These tasks are demonstrated in the following excerpt (which builds upon the previous one): public AnimalTypePage() { initComponents(); } // ... private void initComponents() { buttonGroup1 = new javax.swing.ButtonGroup(); jRadioButton1 = new javax.swing.JRadioButton(); jRadioButton2 = new javax.swing.JRadioButton(); jRadioButton3 = new javax.swing.JRadioButton(); setLayout(new java.awt.GridLayout()); buttonGroup1.add(jRadioButton1); jRadioButton1.setText("Mammal"); jRadioButton1.setBorder(new javax.swing.border.EmptyBorder(new java.awtInsets(0, 0, 0, 0))); jRadioButton1.setMargin(new java.awt.Insets(0, 0, 0, 0)); jRadioButton1.setName("mammal"); add(jRadioButton1); buttonGroup1.add(jRadioButton2); jRadioButton2.setText("Reptile"); jRadioButton2.setBorder(new javax.swing.border.EmptyBorder(new java.awtInsets(0, 0, 0, 0))); jRadioButton2.setMargin(new java.awt.Insets(0, 0, 0, 0)); jRadioButton2.setName("reptile"); add(jRadioButton2); buttonGroup1.add(jRadioButton3); jRadioButton3.setText("Insect"); jRadioButton3.setBorder(new javax.swing.border.EmptyBorder(new java.awtInsets(0, 0, 0, 0))); jRadioButton3.setMargin(new java.awt.Insets(0, 0, 0, 0)); jRadioButton3.setName("insect"); add(jRadioButton3); } Listeners Wizard automatically registers a listener with each standard Swing component that is added to a page. While the user interacts with the component (such as entering text into a text field), the listener is repeatedly invoked and carries out the following tasks: If the component’s name property was set (via the setName() method) prior to adding the component to WizardPage, the component’s value is automatically added to the wizard settings map — name‘s value is used for the map entry’s key. This automatic-listening feature applies to standard Swing components. Check out WizardPage ‘s Javadoc to learn what’s involved in adding this feature to custom components. Whether or not the name property is set, WizardPage‘s validateContents() method is called. You can override this method to enable/disable the Next, Finish, or both buttons; display a message to the user; or perform some other task. Because validateContents() can be called frequently (on every keystroke), the validation code must execute quickly — Wizard provides the org.netbeans.spi.wizard.WizardPanelNavResult class to support slowly executing validation code. A WizardPage subclass must include a public static String getDescription() method. As shown below, this method returns a short string describing the current page. This string appears in the wizard dialog’s sidebar and as the page’s title: public static final String getDescription() { return "Choose a kind of animal"; } Beware of an IllegalArgumentException! If a Wizard subclass doesn’t define the aforementioned getDescription() method, Wizard will throw an IllegalArgumentException at runtime. createWizard() utility methods After defining the WizardPage subclasses, it’s time to create the wizard. Accomplish this task by invoking one of several createWizard() utility methods that WizardPage provides. The WizardPageDemoMain.java excerpt below reveals one of these methods: //All we do here is assemble the list of WizardPage subclasses we //want to show: Class[] pages = new Class[] { AnimalTypePage.class, LocomotionPage.class, OtherAttributesPage.class, FinalPage.class }; //Use the utility method to compose a Wizard Wizard wizard = WizardPage.createWizard(pages, WizardResultProducer.NO_OP); The excerpt takes advantage of WizardPage‘s public static Wizard createWizard(Class[] wizardPageClasses, WizardPage.WizardResultProducer finisher) method to create a wizard. This method requires the following arguments: An array of Class objects. Each Class object in the array must describe a unique WizardPage subclass. At runtime, Wizard lazily creates instances of these classes on an as-needed basis (to reduce object creation). If you want all of these instances to be created up front, invoke a createWizard() method that provides a WizardPage[] parameter. A WizardPage.WizardResultProducer object. This object’s finish() method is invoked after the user clicks the Finish button. This method interrogates user-entered values that are stored in its wizard settings map and accomplishes whatever the wizard was designed to accomplish. The excerpted createWizard() method’s second argument is WizardResultProducer.NO_OP. This constant informs Wizard that the application is not interested in using a wizard result producer to process the settings map. show() methods Each of the createWizard() methods returns an org.netbeans.spi.wizard.Wizard instance that encapsulates a wizard’s logic and state. This class provides several show() methods to start the wizard and reveal its pages. Because the show() methods delegate to the org.netbeans.api.wizard.WizardDisplayer class’s showWizard() methods, you have the option of calling these methods instead, as demonstrated by the following WizardPageDemoMain.java excerpt: //And show it onscreen WizardDisplayer.showWizard (wizard, new Rectangle (10, 10, 600, 400)); System.exit(0); The show() and showWizard() method groups include a method with a java.awt.Rectangle parameter. This parameter allows you to specify the location and size of the wizard’s dialog box. Without this parameter, the dialog box appears at its preferred size. After the user clicks the Finish button, the show()/showWizard() method returns an Object. By default, this object is a java.util.Map instance that contains all of the user-entered values. The previous excerpt ignores this object. Event-dispatching thread and System.exit() Concurrent with Java SE 6’s release, Sun recommended that developers create Swing GUIs on the event-dispatching thread. Although Wizard’s examples don’t appear to do this, I’ll show you how to accomplish this task later. Also, although the System.exit (0); call might appear to be unnecessary, there are times when a GUI-based program doesn’t exit because an internal GUI-related thread continues to run. Input validation Wizards often need to prevent the user from navigating to the next page until the user has made some kind of selection or entered appropriate text (such as a product-registration key) on the current page. For example, an installer wizard might require the user to select an “I agree” radio button on a license-agreement page before enabling the forward navigation (Next) button. Wizard supports this input validation via WizardPage‘s protected String validateContents(Component component, Object event) method. The default version always returns null, which means that the forward-navigation button is never disabled. To disable this button, you need to override this method in a subclass, and have the method return a string that describes why this button is disabled. Localized messages According to Wizard’s documentation, the message returned from validateContents() should be localized to conform to the user’s locale — an English or French message, for example. The documentation also refers to other places where localization should be observed. For brevity, I’ve chosen to avoid localization in this article’s code. This method’s component parameter contains a reference to the component with which the user has interacted. If the component cannot be determined, this parameter contains the null reference. This would happen if validateContents() were invoked in response to calling WizardPage‘s protected final void putWizardData(Object key, Object value) method, for example. The method’s o parameter often contains a reference to the java.util.EventObject or javax.swing.text.DocumentEvent that triggered the call to validateContents(). However, this parameter can also contain the null reference, for the same reason component might contain it. Both parameters are ignored in the following AnimalTypePage.java excerpt: protected String validateContents (Component component, Object o) { //Initially, no radio button is selected, so set the problem string if (!jRadioButton1.isSelected() && !jRadioButton2.isSelected() && !jRadioButton3.isSelected()) { return "You must select an animal"; } else { return null; } } By default, only the Next button is enabled when null returns. However, you can enable Finish (by itself) or both buttons by invoking WizardPage‘s protected final void setForwardNavigationMode(int value) method with an appropriate constant prior to returning null: org.netbeans.spi.wizard.WizardController.MODE_CAN_FINISH indicates that only the Finish button should be enabled. WizardController.MODE_CAN_CONTINUE_OR_FINISH indicates that both Next and Finish should be enabled. Wizard result producers and summary pages Until the user clicks a wizard’s Finish or Cancel button, the wizard gathers user-entered values and stores them in a settings map. Once either button is clicked, the wizard returns this map from show()/showWizard() as a java.util.Map instance, or it invokes an appropriate method defined by a wizard result producer. A wizard result producer is an object whose class implements the WizardPage.WizardResultProducer interface. If this object is passed to the appropriate createWizard() method when creating the wizard, one of its two methods will be invoked in response to the user clicking Finish or Cancel: boolean cancel(Map settings) is invoked in response to the Cancel button being clicked. This method receives the settings map as an argument. It can interrogate these settings, prompt the user to find out if the user really wants to cancel the wizard, and return true to cancel (or false to not cancel). Object finish(Map wizardData) is invoked in response to the Finish button being clicked. This method also receives the settings map as an argument. It can interrogate these settings, carry out various tasks (such as copying files for a program-installer wizard), and return an object (probably composed of settings values) to the application. If a setting has not been validated (perhaps the user hasn’t selected the “I agree” radio button on a license-agreement page), finish() can detect this situation from the settings map and throw an instance of org.netbeans.spi.wizard.WizardException . This instance can contain both a problem description and the name of the step that the wizard returns to so the user can fix the problem. Throwing a WizardException Throwing a WizardException from the finish() method is problematic. Instead of reverting to a previous step in the wizard, I’ve noticed that Wizard throws a java.util.NoSuchElementException in response. After tracking down this exception in the library’s class files, I’ve encountered a MergeMap class whose popAndCalve() method contains a JVM iconst_1 instruction, which I believe should be replaced with iconst_0. The exception problem seems to disappear after making this replacement, although the message passed to WizardException‘s constructor is not displayed. Because this “fix” might inadvertently screw up something else, I’m not presenting it in this article. Hopefully, this WizardException-based problem will be addressed in a future release of the Wizard library. (Editor’s note: It has as of April 2008: See this article’s addendum!) After the finish() method completes its work, it can return any object (or null), which ultimately returns from the show() or showWizard() method that started the wizard. One of the objects that can be returned from finish() is an instance of the org.netbeans.spi.wizard.Summary class. Summary describes objects that contain values to be presented to the user at the end of a wizard. When finish() returns a Summary object, Wizard creates a summary page with this object’s values and presents this page to the user. Wizard also disables all buttons except for Cancel, and replaces this button’s Cancel text with Close. Create a Summary object by calling one of Summary‘s three create() methods. Depending on the chosen method, you can have the summary page present a single item of text via a text control, a list of items via a list control, or arbitrary content via a custom component. Each method lets you specify a result object to be returned from the show()/showWizard() method. Wizard result producer and summary page demonstration Because neither of Wizard’s basic or advanced examples demonstrates a wizard result producer and a summary page, I’ve created a simple demonstration wizard. This demonstration consists of two source files: DemoPage.java and WizDemo1.java. Listing 1 presents the former source file’s contents. Listing 1. DemoPage.java // DemoPage.java import java.awt.*; import javax.swing.*; import org.netbeans.spi.wizard.*; public class DemoPage extends WizardPage { private JTextField txtField1; private JTextField txtField2; public DemoPage () { setLayout (new BorderLayout ()); JPanel pnl = new JPanel (); pnl.add (new JLabel ("First name")); txtField1 = new JTextField (20); txtField1.setName ("first"); pnl.add (txtField1); add (pnl, BorderLayout.NORTH); pnl = new JPanel (); pnl.add (new JLabel ("Last name")); txtField2 = new JTextField (20); txtField2.setName ("last"); pnl.add (txtField2); add (pnl); } public static final String getDescription () { return "Demonstration page"; } protected String validateContents (Component comp, Object o) { if (txtField1.getText ().length () == 0) return "Enter the first name"; else if (txtField2.getText ().length () == 0) return "Enter the last name"; else return null; } } DemoPage1 describes the wizard’s solitary page, which consists of two labeled text fields. Each time a character is entered into either text field, validateContents() is invoked to ensure that both text fields contain at least one character. An appropriate error message is returned for display at the bottom of the page if a text field is empty. Listing 2 presents WizDemo1.java. Listing 2. WizDemo1.java // WizDemo1.java import java.awt.*; import java.util.*; import org.netbeans.api.wizard.WizardDisplayer; import org.netbeans.spi.wizard.*; public class WizDemo1 { public static void main (String [] args) { final Class [] pages = new Class [] { DemoPage.class }; Runnable r; r = new Runnable () { public void run () { MyProducer mp = new MyProducer (); Wizard wizard = WizardPage.createWizard (pages, mp); System.out.println ("Result = "+ WizardDisplayer.showWizard (wizard)); System.exit (0); } }; EventQueue.invokeLater (r); } } class MyProducer implements WizardPage.WizardResultProducer { public Object finish (Map wizardData) throws WizardException { System.out.println ("finish called"); System.out.println (wizardData); String [] items = new String [2]; items [0] = "First name: "+wizardData.get ("first"); items [1] = "Last name: "+wizardData.get ("last"); return Summary.create (items, null); } public boolean cancel (Map settings) { System.out.println ("cancel called"); System.out.println (settings); return true; // Allow the user to cancel the wizard. } } WizDemo1 creates a wizard consisting of the single DemoPage wizard page and a wizard result producer. (I’ve decided to create and show the wizard on the event-dispatching thread, which I mentioned in an earlier note.) After you’ve entered first and last names, Wizard enables the Finished button. Clicking this button results in the summary page. Background processing Wizard invokes a wizard result producer’s finish() method to carry out the wizard’s objective. It’s possible that accomplishing this objective might involve a time-consuming operation such as fetching content over the Internet. Because the finish() method is invoked on the event-dispatching thread, this activity results in an unresponsive GUI. You could overcome this problem by taking advantage of Java SE 6’s new javax.swing.SwingWorker class, but this feature isn’t available to previous Java versions. Fortunately, Wizard provides its own solution, based on its abstract org.netbeans.spi.wizard.DeferredWizardResult class and org.netbeans.spi.wizard.ResultProgressHandle interface. DeferredWizardResult and ResultProgressHandle work together to keep the GUI responsive, and to present a progress bar that shows the user how much of a task has been completed, and how much of a task is left to complete. Begin using these types by creating a class that subclasses DeferredWizardResult. Your subclass will need to implement DeferredWizardResult‘s public abstract void start(Map settings, ResultProgressHandle progress) method. Wizard invokes this method on a thread other than the event-dispatching thread, with settings referring to a map of user-entered values, and progress referring to an object for interacting with the progress bar. The start() method periodically invokes ResultProgressHandle‘s public void setProgress(int currentStep, int totalSteps) or void setProgress(String description, int currentStep, int totalSteps) method to update the progress bar before performing part (if not all) of the task. The latter method lets you specify a message to be displayed over the progress bar. When a task finishes properly, start() invokes ResultProgressHandle‘s public void finished(Object result) method. Its result argument references an object to be returned from show()/showWizard(). However, this object can also be a Summary object, which is treated specially by Wizard (as you learned in the previous section). It’s possible that the task might fail to finish (perhaps an exception is thrown). In this case, start() must invoke ResultProgressHandle‘s public void failed(String message, boolean canNavigateBack) method, with message providing text to present to the user, and canNavigateBack indicating whether or not the Prev button should be enabled. Watch out for a runtime exception! Do not invoke the finished() method after invoking failed() (or vice-versa). If you do, a runtime exception might be thrown. The only other essential task is to create an object from the DeferredWizardResult subclass and return this object from the wizard result producer’s finished() method. When the user clicks the Finish button, and finish() returns this object, Wizard creates a background thread and invokes the subclass’s start() method on this thread. Listing 3 presents WizDemo2.java, which prepares a wizard for background processing. Listing 3. WizDemo2.java // WizDemo2.java import java.awt.*; import java.util.*; import org.netbeans.api.wizard.WizardDisplayer; import org.netbeans.spi.wizard.*; public class WizDemo2 { public static void main (String [] args) { final Class [] pages = new Class [] { DemoPage.class }; Runnable r; r = new Runnable () { public void run () { MyProducer mp = new MyProducer (); Wizard wizard = WizardPage.createWizard (pages, mp); System.out.println ("Result = "+ WizardDisplayer.showWizard (wizard)); System.exit (0); } }; EventQueue.invokeLater (r); } } class MyProducer implements WizardPage.WizardResultProducer { public Object finish (Map wizardData) throws WizardException { System.out.println ("finish called"); System.out.println (wizardData); return new Result (); } public boolean cancel (Map settings) { System.out.println ("cancel called"); System.out.println (settings); return true; // Allow the user to cancel the wizard. } } class Result extends DeferredWizardResult { public Result () { // Uncomment the following line to make it possible to close the dialog // while the operation is running (abort the operation, in other words). // super (true); } public void start (Map settings, ResultProgressHandle progress) { progress.setProgress ("Phase 1", 0, 3); try { Thread.sleep (3000); } catch (InterruptedException ie) { } progress.setProgress ("Phase 2", 1, 3); try { Thread.sleep (1000); } catch (InterruptedException ie) { } progress.setProgress ("Phase 3", 2, 3); try { Thread.sleep (2000); } catch (InterruptedException ie) { } String [] items = new String [2]; items [0] = "First name: "+settings.get ("first"); items [1] = "Last name: "+settings.get ("last"); // Replace null with an object reference to have this object returned // from the showWizard() method. progress.finished (Summary.create (items, null)); } } WizDemo2 creates a wizard that shares WizDemo1‘s DemoPage wizard-page class. To simulate a time-consuming activity, the start() method in this wizard’s Result class contains three thread-sleep calls. Notice the setProgress() method calls, which keep the user informed of the task’s progress by updating a progress bar. An alternative to WizardPage The org.netbeans.spi.wizard.WizardPanelProvider class is an alternative to WizardPage for defining a wizard’s pages. According to Wizard’s FAQ, “the main difference is with WizardPanelProvider, you don’t have to subclass a Swing panel component — you can just create a new JPanel, add what you want and return it. This is useful for legacy code, migrating from other wizard frameworks.” To work with WizardPanelProvider, begin by subclassing this class. The subclass’s constructor will need to invoke one of WizardPanelProvider‘s three constructors, such as protected WizardPanelProvider(String title, String[] steps, String[] descriptions). This constructor creates an instance of the subclass with the passed title, steps, and descriptions: The title argument provides a human-readable title that appears in the title bar of the wizard’s dialog box. The steps argument provides an array of strings that identify the various steps performed by the wizard. Each step is associated with one of the wizard’s pages. The descriptions argument provides an array of strings that describe the various steps and are presented to the user. There is a 1:1 correspondence between the steps and descriptions. Continue by overriding WizardPanelProvider‘s protected abstract JComponent createPanel(WizardController controller, String id, Map settings) method, which is responsible for creating and returning all of the wizard’s panels (also known as pages) — Wizard invokes this method once for each panel to be created. This method’s arguments are described below: The controller argument controls whether the Next and Finished buttons are enabled, and also what problem text is displayed to the user. The id argument identifies one of the steps passed to the constructor via steps. The createPanel() method determines which panel to create by interrogating this argument. The settings argument identifies the wizard’s settings map. As the user interacts with the current panel’s components, various settings are updated with the latest component values. Along with createPanel(), the subclass can implement WizardPanelProvider‘s protected Object finish(Map settings) method to complete the wizard and return whatever object is meaningful to the code that invoked show()/showWizard(). This method is identical to WizardPage.WizardResultProducer‘s finish() method (which I discussed earlier). After defining the WizardPanelProvider subclass, it’s time to create the wizard. Accomplish this task by invoking WizardPanelProvider‘s public final Wizard createWizard() method. Both this method and the definition of a WizardPanelProvider subclass are demonstrated in Listing 4’s WizDemo3.java source code. Listing 4. WizDemo3.java // WizDemo3.java import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import javax.swing.event.*; import org.netbeans.api.wizard.WizardDisplayer; import org.netbeans.spi.wizard.*; public class WizDemo3 { public static void main (String [] args) { Runnable r; r = new Runnable () { public void run () { MyProvider mp = new MyProvider (); Wizard wizard = mp.createWizard (); Object result; result = WizardDisplayer.showWizard (wizard, new Rectangle (100, 100, 500, 200)); System.out.println ("Result = "+result); System.exit (0); } }; EventQueue.invokeLater (r); } } class MyProvider extends WizardPanelProvider { public MyProvider () { // Pass a title (which should be localized) for the wizard. Also pass an // array of IDs for the wizard's two steps, and an array of descriptions // that describe these steps (and that should be localized). super ("Get name and status", new String [] { "name", "status" }, new String [] { "Enter your name", "Check if age 65 or over" }); } // The following method creates a panel for each wizard page. It's called // only once (during the life of the wizard) for each of the name and // status IDs. protected JComponent createPanel (final WizardController controller, String id, final Map settings) { if ("name".equals (id)) { // Ensure that all buttons are disabled until the user has entered a // name. controller.setProblem ("Name must be entered"); // Create the text field and register a caret listener whose // caretUpdate() method is invoked each time a character is entered // into this field. final JTextField name = new JTextField (20); CaretListener cl; cl = new CaretListener () { public void caretUpdate (CaretEvent ce) { // Save the current name in the settings map. settings.put ("name", name.getText ()); if (name.getText ().length () == 0) controller.setProblem ("Name must be entered"); else controller.setProblem (null); // Enable Next. } }; name.addCaretListener (cl); // Wrap the text field in a panel to ensure that the text field // appears at its preferred size. JPanel panel = new JPanel (); panel.add (name); return panel; } else { // Create a checkbox and register an action listener whose // actionPerformed() method is invoked each time the checkbox is // selected/deselected. final JCheckBox status = new JCheckBox (); ActionListener al; al = new ActionListener () { public void actionPerformed (ActionEvent ae) { // Save the current status selection in the settings // map. settings.put ("status", status.isSelected () ? Boolean.TRUE : Boolean.FALSE); } }; status.addActionListener (al); return status; } } protected Object finish (Map settings) throws WizardException { System.out.println ("finish called"); String [] items = new String [2]; items [0] = "Name = "+settings.get ("name"); items [1] = "Status = "+settings.get ("status"); return Summary.create (items, null); } } WizDemo3.java describes a wizard consisting of two pages: one page for entering a name and another page for indicating a “65 or older” age status. The name must be entered, otherwise controller.setProblem ("Name must be entered"); informs the user of the problem and prevents the Next button from being enabled. Unlike the name, the user isn’t required to select the status checkbox. This leads to an interesting situation when retrieving the status field in the finish() method. If the checkbox is never checked, the settings map won’t contain a status entry and settings.get ("status") returns null. But if the checkbox is checked (and possibly unchecked), this method call returns true (or false). Meta wizards You might occasionally need to create a dynamic wizard that chooses (based on user input) one of several alternative sequences of steps to execute after executing an initial sequence. For example, after executing the A-B sequence (step A followed by step B), a dynamic wizard might need to choose between executing the C-D-E sequence or the F-G sequence. In its answer to a question on multiple branch points, Wizard’s FAQ points out that a fixed sequence of steps is one wizard, each of several alternative wizards is a branch, and the step that leads to one of these branches is a branch point. Furthermore, it points out that the resulting wizard encompassing all of these wizards is a meta wizard. Define a meta wizard by defining each branch via WizardPage or WizardPanelProvider, by subclassing the abstract org.netbeans.spi.wizard.WizardBranchController class for each branch point, by passing the branch leading to a branch point to the WizardBranchController superclass via one of its constructors, and by overriding one of the following methods in each subclass: protected WizardPanelProvider getPanelProviderForStep(String step, Map settings) protected Wizard getWizardForStep(String step, Map settings) According to WizardBranchController‘s Javadoc, you can “override getPanelProviderForStep to provide instances of WizardPanelProvider if there are no subsequent branch points and the continuation is a simple wizard.” Alternatively, you can “override getWizardForStep to return one or another wizard which represents the subsequent steps after a decision point.” The meta wizard tries to find a branch via getWizardForStep(). This method defaults to invoking getPanelProviderForStep(), which defaults to throwing an error. If you override either method, it should return null if step doesn’t indicate a branch point (such as B in the earlier A-B sequence). Otherwise, the method uses settings to determine the appropriate branch, which is returned to the meta wizard. Yet another createWizard() method! WizardBranchController specifies a public final Wizard createWizard() method for creating a wizard based on its branch controller instance. This is all I have to say about meta wizards. If you’d like to continue exploring this topic, check out WizardBranchController‘s Javadoc and the FAQ’s multi-branch meta-wizard example, which demonstrates subclassing WizardBranchController and overriding getWizardForStep(). Also, check out Wizard’s advanced example. Make an installer Installer programs are a great example of wizard usefulness. Installers employ wizards to gather installation details from users, and then use those details while installing their bundled programs. I think the best way to demonstrate the usefulness of the Wizard project is to use Wizard in an installer context, so I’ve created a MakeInstaller application example for you to learn from. MakeInstaller provides a Wizard-based wizard that prompts its user for details related to making an installer. For example, it prompts for the location and name of an executable JAR file (a JAR file whose manifest names a main class, and which executes via the java command’s -jar option). Presumably, this JAR file contains a Java application that a developer wishes to distribute. After obtaining the JAR file’s location and name, the location and name of a text-based license-agreement file, and other details, MakeInstaller creates an executable JAR file named Installer.jar (in response to clicking the Finish button). This JAR file contains the previous JAR file, the license-agreement text file, and several class files that describe an installer. Figure 6 reveals MakeInstaller‘s summary page. Figure 6. The summary page presents a message stating MakeInstaller’s success or failure in making Installer.jar. (Click to enlarge.) This article’s code archive includes a MakeInstaller directory that contains MakeInstaller‘s six source files: GetDefInstLocPage.java, GetJARPage.java, GetLAPage.java, GetTitlePage.java, InstallerGenerator.java, and MakeInstaller.java. For brevity, let’s focus only on MakeInstaller.java. Listing 5 presents this file’s contents. Listing 5. MakeInstaller.java // MakeInstaller.java import java.awt.*; import java.util.*; import javax.swing.*; import org.netbeans.api.wizard.WizardDisplayer; import org.netbeans.spi.wizard.*; public class MakeInstaller { public static void main (String [] args) { final Class [] pages = new Class [] { GetTitlePage.class, GetLAPage.class, GetJARPage.class, GetDefInstLocPage.class }; Runnable r; r = new Runnable () { public void run () { MakeInstallerProducer mip = new MakeInstallerProducer (); Wizard wizard = WizardPage.createWizard ("Make Installer", pages, mip); WizardDisplayer.showWizard (wizard, new Rectangle (20, 20, 600, 200)); System.exit (0); } }; EventQueue.invokeLater (r); } } class MakeInstallerProducer implements WizardPage.WizardResultProducer { public Object finish (Map wizardData) throws WizardException { String title = (String) wizardData.get ("title"); String lapath = (String) wizardData.get ("lapath"); String jarpath = (String) wizardData.get ("jarpath"); String definstlocpath = (String) wizardData.get ("definstlocpath"); InstallerGenerator.configure (title, lapath, jarpath, definstlocpath); String result = InstallerGenerator.makeInstaller (); if (result != null) return Summary.create (new JLabel ("Unable to make installer: "+ result, JLabel.CENTER), null); else return Summary.create (new JLabel ("Installer created!", JLabel.CENTER), null); } public boolean cancel (Map settings) { return true; // Allow the user to cancel the wizard. } } For simplicity, MakeInstaller gathers only an application title, the locations and names of the license-agreement and JAR files, and a default installation location. These values are stored in the settings map under the title, lapath, jarpath, and definstlocpath keys, respectively. If desired, additional details such as a readme file’s name and location could be gathered. After retrieving these values from the settings map, MakeInstallerProducer interacts with the InstallerGenerator class via this class’s public static void configure(String title, String lapath, String jarpath, String definstlocpath) and public static String makeInstaller() methods: configure() configures the generator with the wizard-selected values. makeInstaller() makes Installer.jar. If something goes wrong, this method returns a string identifying the problem. It returns null if everything is fine. While developing MakeInstaller, I considered various ways to create Installer.jar. I settled on an approach that takes advantage of Java SE 6’s Compiler API. The makeInstaller() method hardcodes various classes as strings and uses the Compiler API to compile these string-based classes into their equivalent class files, which are stored in the current directory. Watch out for the JRE! The compiler is accessed via javax.tools.ToolProvider‘s public static JavaCompiler getSystemJavaCompiler() method. This method returns the null reference if a Java compiler isn’t present. For example, this method returns null if executed on a machine where only the JRE is installed. Instead of creating Installer.jar programmatically via the java.util.jar package’s types, makeInstaller() executes the JDK’s jar program. After creating the JAR file via jar, this method performs cleanup by deleting the class files, along with copies of the license-agreement text file and the JAR file being bundled into Installer.jar. Enough theory! Let’s play with MakeInstaller. Before doing this, you’ll need to compile its source code and run this application. For example, after changing to the code archive’s MakeInstaller directory, I invoke the first two commands below (on my Windows XP platform) to accomplish the first two tasks, and the third command (which should also work unchanged on other platforms) to run the installer: javac -cp wizard.jar;. MakeInstaller.java java -cp wizard.jar;. MakeInstaller java -cp wizard.jar -jar Installer.jar Battle on the high seas I’ve created a Sea Battle application for the purpose of demonstrating MakeInstaller. Sea Battle’s Java SE 6 executable and source codes are stored in an executable JAR file named SeaBattle.jar, which is located in the sample subdirectory of the code archive’s MakeInstaller directory. From applet to application Sea Battle originated as an applet for my JavaWorld article “Java Fun and Games: It’s contest time.” In addition to converting this applet to an application, I modified the source code to reflect generics and Java SE 6 — the original applet was written using Java 1.4. Assume that SeaBattle.jar is an open source project developed under the GNU General Public License — I’ve included this license’s gpl.txt file in the sample subdirectory. After starting MakeInstaller (as demonstrated earlier), complete the following steps to generate an Installer.jar file that includes gpl.txt and SeaBattle.jar: Enter Sea Battle as the application title and click Next to continue. Enter gpl.txt with its path into the text field, or use the Browse button. Click Next to continue. Enter SeaBattle.jar with its path into the text field, or use the Browse button. Click Next to continue. Enter a default installation location (such as c:seabattle for Windows XP) into the text field. Click Finish to create Installer.jar. If all goes well, you should see a summary page similar to the one shown in Figure 6. Invoke Installer.jar (as demonstrated earlier). The wizard’s initial page welcomes you to the installer, and presents the application title previously specified on the MakeInstaller wizard’s initial page. The second page presents gpl.txt‘s license information, and a radio button that you must select to continue. The final page lets you choose the installation location (or keep the default). After clicking Finish, Installer.jar creates the installation directory and copies SeaBattle.jar to that location, and that is all. (I considered creating a .lnk file that contains the Windows XP shortcut link for running SeaBattle.jar, and copying this file to the XP desktop, but decided against these tasks because they are platform-specific.) Invoke java -jar SeaBattle.jar to run Sea Battle. In conclusion There’s more to Wizard’s API for you to explore. For example, you can use WizardDisplayer‘s installInContainer() method to install a wizard in a container other than the standard dialog box. Also, you can replace the sidebar’s background image. The “Customizing the default implementation” section of WizardDisplayer‘s Javadoc presents two techniques for accomplishing this image-replacement task. Your own WizardDisplayer Wizard lets you replace the default implementation of WizardDisplayer with your own implementation, allowing you to completely replace Wizard’s user interface. Check out the “Providing a custom WizardDisplayer” section in WizardDisplayer‘s Javadoc for more information. Despite some disappointment in not being able to revert to a previous step in a wizard, by throwing a WizardException from WizardPage.WizardResultProducer‘s finish() method (which I believe is caused by a small bug), I’ve found Wizard to be a powerful tool that’s easy to work with. Most importantly, Wizard takes the pain out of creating Swing-based wizards. Addendum: Wizard exception fixed! Tim Boudreau has released a new version of his Wizard library (version 0.997) that fixes the problem related to throwing a WizardException from the finish() method. Instead of throwing an unrelated exception, the new version displays a message box with the message passed as the first argument to WizardException‘s two-parameter constructor. Clicking the message box’s OK button causes Wizard to revert the wizard’s GUI to the GUI associated with the step whose ID is passed as the second argument to the two-parameter constructor. I’ve created a WizardExceptionDemo application (find the code bundled with the rest of this article’s code) that demonstrates throwing WizardException in the context of the latest Wizard library. This application presents two pages — the first page requests first and last names; the second page requests address and city. You must enter a first name, but entering a last name, address, and city is optional. When you click the Finish button, the wizard result producer’s finish() method (shown below) is invoked: public Object finish (Map wizardData) throws WizardException { System.out.println ("finish called"); System.out.println (wizardData); String value = (String) wizardData.get ("last"); if (value.length () == 0) throw new WizardException ("Enter the last name", wizard.getAllSteps () [0]); return null; // This value returns from show()/showWizard (). } If finish() detects that a last name has not been entered, it creates a WizardException object with a descriptive error message (that should be localized) and the ID of the step to revert to, which happens to be the step that prompts for first and last names. In response to throwing this exception, Wizard presents the message box that is shown in Figure 8. Thanks, Tim, for making these helpful changes to the Wizard API! Jeff Friesen is a freelance software developer and educator who specializes in Java technology. Check out his javajeff.mb.ca website to discover all of his published Java articles and more. JavaOpen SourceSoftware DevelopmentAPIs