Saturday 21 June 2014

Multiple files from a single wizard in NetBeans

I've been fighting with NetBeans modules recently. I wanted to add templates for common class types used in Zaria. Half of them, in order to be useful, go in pairs: Object + ObjectDefinition.

To make the process as easy as possible, I wanted to be able to crate both files using a single dialog or wizard. Meaning: If I choose to create a new EntityComponent then both NewEntityComponent.java and NewEntityComponentDefinition.java get created with all boilerplate code in place.

I managed to do that. However, either because of bugs or weird design decisions, it is far from straightforward.

Ok. So how to do it?


First have a look at https://platform.netbeans.org/tutorials/nbm-filetemplates.html

It shows how to create a new module that will generate a new file from a template. It is a good beginning and in theory should be all you need.

The annotation @TemplateRegistration in the field content can take multiple files. Spec says that the default New File wizard should then create both files using the input templates.

However the reality is different:
  • Only the first file is created. To fix this you need to create your own custom wizard.
  • If you want to generate java classes from templates, you need to call the template file xxx.java.template Otherwise the module will not compile.
 

How to create your custom wizard?


Although it shows more than needed here, a good initial tutorial can be found at https://platform.netbeans.org/tutorials/nbm-wizard.html

In the New Wizard dialog choose New File instead of Custom. This will remove some of the complications and will make the wizard automatically work with New File menu in NetBeans.

Annotate your wizard as in the first tutorial:
   @TemplateRegistration(  
     folder = "Zaria",  
     iconBase="eu/cherrytree/zaria/modules/filetemplates/zaria_file.png",  
     displayName = "#EntityController_displayName",  
     content = {"EntityController.java.template" , "EntityControllerDefinition.java.template"},  
     description="EntityControllerDescription.html",  
     scriptEngine = "freemarker")  

   @NbBundle.Messages("EntityController_displayName=Entity Controller")  


And the whole trick is in the instantiate() method in your WizardIterator:
    
     private FileObject getDefinition(FileObject fileObject)  
     {  
         for(int i = 0 ; i < fileObject.getParent().getChildren().length ; i++)  
         {  
             FileObject obj = fileObject.getParent().getChildren()[i];  
               
             if(obj.getName().equals(fileObject.getName() + "Definition"))  
                 return obj;  
         }  
           
         return null;  
     }  
       
     @Override  
     public Set<?> instantiate() throws IOException  
     {  
         // Prepare the arguments for passing to the FreeMarker template.  
         String name = (String) wizard.getProperty(GameObjectVisualPanel.nameProperty);  
         Map<String, Object> args = new HashMap<String, Object>();  
         args.put("name", name);  
   
         //Get the first template.  
         FileObject firstTemplate = Templates.getTemplate(wizard);  
           
         // Find the second template.  
         FileObject secondTemplate = getDefinition(firstTemplate);  
                   
         DataObject dTemplate1 = DataObject.find(firstTemplate);  
         DataObject dTemplate2 = DataObject.find(secondTemplate);          
   
         // Force usage of the script engine.  
         secondTemplate.setAttribute("javax.script.ScriptEngine", firstTemplate.getAttribute("javax.script.ScriptEngine"));  
   
         // Get the package.  
         FileObject dir = (FileObject) wizard.getProperty(GameObjectVisualPanel.pathProperty);  
         DataFolder df = DataFolder.findFolder(dir);  
   
         // Set the names of the file:  
         String targetName1 = name;  
         String targetName2 = name + "Definition";  
   
         // Define the templates from the above, passing the package, the file name, and the map of strings to the template:  
         dTemplate1.createFromTemplate(df, targetName1, args);  
         dTemplate2.createFromTemplate(df, targetName2, args);  
   
         // End.  
         return Collections.EMPTY_SET;  
     }  


It would seem that there are two issues:
  • The second file in the TemplateRegistration is quirky. It would seem that the templates are always put one after another. There no are guarantees when it comes to ordering of FileObjects in regads to templates. Therefore you will need your own trick to associate them with each other. In my case name is sufficient.
  • The script engine is only assigned to the first file. Therefore you have to copy the ScriptEngine attribute from the first template to the others.
With these two tricks everything seems to be working fine :)


BTW. If you need to get the currently selected source directory, use:
         Project project = Templates.getProject(wizard);  
         Sources sources = ProjectUtils.getSources(project);  
         SourceGroup sourceGroup = sources.getSourceGroups("java")[0];  
               
         FileObject src_root = sourceGroup.getRootFolder(); 

I've been testing this in jMonkey Engine SDK 3.0 which is modified version of NetBeans 7.x. Maybe NetBeans 8.x does not have this issues. However the Internet suggests that as of June 2014 people are still having problems.