Tuesday Apr 01, 2008

Creating a Better Java Class Wizard

At the end of my recent Javalobby article, Overview of NetBeans Java API Samples in Plugin Portal, two users asked for a more advanced wizard for Java classes, one that would let the user specify superclasses and interfaces right in the wizard. Fine. Let's create exactly that. If you work through everything in this blog entry, you will end up with a new template in the New File dialog:

When you click Next, you'll get everything as usual, except also the possibility to fill in superclasses and interfaces (all on the same panel, hurray):

On clicking Finish, you'll have a new class, with the superclasses and interfaces you specified generated automatically right into the class, and lightbulbs to click through to generate the import statements and required methods:

If you need this, or something like that, follow along below:

  1. Create a new module, called "FancyJavaClassTemplate", with code name base "org.netbeans.modules.fancy".

  2. In the New File dialog, use the Wizard wizard to create stubs for a new wizard, with "New File" under "Registration Type" and "1" in "Number of Wizard Panels":

  3. Type values in the final panel and click Finish. Then look at the generated sources. First look at the layer.xml file, then at the Java classes, and then at nbproject/project.xml.

  4. Use FancyVisualPanel1 to design the extra panel that you want to show, making sure that you call the first field "extField" and the second "impField" (because the code later on in this blog entry will assume that you've done so):

    Don't worry about the top part of what you see in the second screenshot above, because that's simply reused from the NetBeans internals, as you will see.

  5. Set dependencies on 'Datasystems API', 'Filesystem API', 'Java project Support', 'Nodes API', 'Project API', and 'Project UI API'.

  6. Open the class called "FancyWizardIterator.java". Rewrite the start of the getPanels() method as follows and make sure to also declare the packageChooserPanel, as shown below:

    private WizardDescriptor.Panel packageChooserPanel;
    
    private WizardDescriptor.Panel[] getPanels() {
        Project project = Templates.getProject(wizard);
        Sources sources = (Sources) project.getLookup().lookup(Sources.class);
        SourceGroup[] groups = sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
        packageChooserPanel = JavaTemplates.createPackageChooser(project, groups, new FancyWizardPanel1());
        if (panels == null) {
            panels = new WizardDescriptor.Panel[]{
                        packageChooserPanel,
                    };
            String[] steps = createSteps();
            ...
            ...
            ...

    Fix imports and make sure that you have ALL of these as your import statements:

    import java.awt.Component;
    import java.io.IOException;
    import java.util.Collections;
    import java.util.NoSuchElementException;
    import java.util.Set;
    import javax.swing.JComponent;
    import javax.swing.event.ChangeListener;
    import org.netbeans.api.java.project.JavaProjectConstants;
    import org.netbeans.api.project.Project;
    import org.netbeans.api.project.SourceGroup;
    import org.netbeans.api.project.Sources;
    import org.netbeans.spi.project.ui.templates.support.Templates;
    import org.openide.WizardDescriptor;

  7. Install and see the results. You should see the panels shown at the start of this blog entry. But nothing will happen when you click Finish, yet. Probably the icon will be different. (You can set that by copying from another template, if you expand "this layer in context" in the layer.xml, then find another template that has the icon you want and choose "Open Layer File(s)". Then copy the attribute that points to the icon.)

  8. Go to the Template Manager under the Tools menu and open Classes | Java Class into the editor. You should see this:

    <#assign licenseFirst = "/\*">
    <#assign licensePrefix = " \* ">
    <#assign licenseLast = " \*/">
    <#include "../Licenses/license-${project.license}.txt">
    
    <#if package?? && package != "">
    package ${package};
    
    </#if>
    /\*\*
     \*
     \* @author ${user}
     \*/
    public class ${name} {
    
    }

    Create a new empty file, call it "Fancy.ftl", and then copy all of the above into it. Then add the bit in bold below:

    <#assign licenseFirst = "/\*">
    <#assign licensePrefix = " \* ">
    <#assign licenseLast = " \*/">
    <#include "../Licenses/license-${project.license}.txt">
    
    <#if package?? && package != "">
    package ${package};
    
    </#if>
    /\*\*
     \*
     \* @author ${user}
     \*/
    public class ${name} ${extends} ${implements} {
    
    }

  9. Open the layer.xml file and tweak it so that it has the same content as the below. Pay particular attention to the bits that are in bold and make VERY sure that your layer.xml file has these too after you've finished tweaking it:

    <filesystem>
        <folder name="Templates">
            <folder name="Classes">
                <file name="fancy.java" url="Fancy.ftl">
                    <attr name="SystemFileSystem.localizingBundle" stringvalue="org.netbeans.modules.fancy.Bundle"/>
                    <attr name="instantiatingIterator" newvalue="org.netbeans.modules.fancy.FancyWizardIterator"/>
                    <attr name="template" boolvalue="true"/>
                    <attr name="SystemFileSystem.icon" urlvalue="nbresloc:org/netbeans/modules/java/resources/class.gif"/>
                    <attr name="templateWizardURL" urlvalue="nbresloc:/org/netbeans/modules/fancy/fancy.html"/>
                    <attr name="javax.script.ScriptEngine" stringvalue="freemarker"/>
                </file>
            </folder>
        </folder>
    </filesystem>

    In other words, make sure that the first line in bold above has ".java" as the name's extension and that "Fancy.ftl" is set as the URL. Check to make sure that your template has exactly that name. It is case sensitive, so be careful. The second line in bold above is needed because that activates the FreeMarker scripting engine, which is what you need to have the NetBeans Platform do for you in order for the template to be generated correctly.

  10. Now we will pass the values from the panel into the template above. Firstly, in the visual panel, make the values in the text fields accessible:

    public String getExtension() {
        return extField.getText();
    }
    
    public String getImplementation() {
        return impField.getText();
    }

    Also add two fields to the top of the visual panel class:

    static final String EXTENSION = "extension";
    static final String IMPLEMENTATION = "implementation";

    In the wizard panel (i.e., NOT the visual panel), retrieve the above values:

    private String getExtensionFromVisualPanel() {
        return ((FancyVisualPanel1) component).getExtension();
    }
    
    private String getImplementationFromVisualPanel() {
        return ((FancyVisualPanel1) component).getImplementation();
    }

    Store the value by overriding the store method in the wizard panel:

    @Override
    public void storeSettings(Object settings) {
        ((WizardDescriptor) settings).putProperty(FancyVisualPanel1.EXTENSION,  getExtensionFromVisualPanel());
        ((WizardDescriptor) settings).putProperty(FancyVisualPanel1.IMPLEMENTATION,  getImplementationFromVisualPanel());
    }

  11. To wrap it all up together, implement the iterator's instantiate() method as follows:

    public Set instantiate() throws IOException {
    
        //Prepare the arguments for passing to the FreeMarker template:
        String extension = (String) wizard.getProperty(FancyVisualPanel1.EXTENSION);
        String implementation = (String) wizard.getProperty(FancyVisualPanel1.IMPLEMENTATION);
        Map args = new HashMap();
        args.put("extends", "extends " + extension);
        args.put("implements", "implements " + implementation);
    
        //Get the template and convert it:
        FileObject template = Templates.getTemplate(wizard);
        DataObject dTemplate = DataObject.find(template);
    
        //Get the package:
        FileObject dir = Templates.getTargetFolder(wizard);
        DataFolder df = DataFolder.findFolder(dir);
    
        //Get the class:
        String targetName = Templates.getTargetName(wizard);
    
        //Define the template from the above,
        //passing the package, the file name, and the map of strings to the template:
        DataObject dobj = dTemplate.createFromTemplate(df, targetName, args);
    
        //Obtain a FileObject:
        FileObject createdFile = dobj.getPrimaryFile();
    
        //Create the new file:
        return Collections.singleton(createdFile);
    
    }

  12. You're finished! Install the plugin and now the template should create the file for you.

Homework assignments—figure out what should happen when the user types NOTHING in one of the new fields in the wizard. Should you do something in the code to catch that? The wizards can provide validation functionality, which you would need to enable, causing red error messages to appear in the panel under specified conditions, such as the fields not being (correctly) filled out. Or... should you make use of the FreeMarker template to add logic to it there so that if an empty string is passed, nothing is generated? Also, figure out how to HIDE the existing Java class wizard and let your own wizard replace it.

Further resources—FreeMarker Template Sample in the Plugin Portal, which gives you additional samples for wizards just like this (including one for generating an HTML file), as well as syntax coloring and code completion for FreeMarker templates.

Today on NetBeans Zone. How Fast Can OpenOffice.org Be Extended?

About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« April 2008 »
SunMonTueWedThuFriSat
  
2
4
5
13
18
19
23
30
   
       
Today