Chapter 3 - Integrating Custom Templates

The Developer Edition of Poseidon for UML enables you to modify the standard templates or add your own templates for code generation. These custom templates can add new features to already existing language templates, or even provide code generation for a completely new language. After a brief overview of the code generation process in general, the following section gives a short introduction on how to enhance the code generation with your own templates, and how to integrate your custom templates into the code generation framework.



Customizing the Standard Templates

Please refer to the Velocity documentation for a description of the template language. The standard templates supplied with Poseidon are a good starting point for learning the Velocity template language and the interface to Poseidon, as they provide numerous examples of how to reference prepared elements from within a template. One way of customizing the code generation in Poseidon is to modify these standard templates. For example, you could create your own custom versions of JavaClass.vm and LIB_StaticStructure.vm and integrate these customized templates into the code generation. Short of overwriting the template files supplied with Poseidon, here is how you can configure Poseidon to use a modified set of templates:

  1. Create a copy of the template directory including the standard template files, for example:

    Standard template directory:
    [PoseidonInstallationFolder]/poseidon1.2/lib/templates

    Your custom template directory:
    [PoseidonInstallationFolder]/poseidon1.2/lib/custom/templates

  2. Open the code generation dialog, and change the setting for Template Directory so that it points to your customized templates.

    Please note: The subdirectory /templates and the names of the standard template files are automatically appended to this setting. For our example the correct entry in the Template Directory field would be:
    [PoseidonInstallationFolder]/poseidon1.2/lib/custom.

  3. Click Apply to save your settings. The next time you start Poseidon the templates will be loaded from the specified custom template directory.



Integrating New Templates

You can also create your own custom templates and add these to Poseidon's code generation framework using the plugin mechanism. Your plugin will register the templates with Poseidon when it is installed, and remove them when it is uninstalled. Please refer to the documentation of the plugin mechanism for details on how to write a plugin. This section will focus on the communication of the plugin with the code generation framework.

The following diagram illustrates the control flow within the program between the participating classes:


The code generation process is started when a user presses the Generate Code button. After updating the settings - notably, writing them to a place where they will be saved persistently, and can be recovered at the next run of Poseidon - the controller of the CodeGenerationDialog calls the GeneratorFactory to retrieve the GenerationController that should be used for this run. Which GenerationController is used depends on the current language settings which can be accessed in poseidon.codeGen.language.

The GenerationController is the entry point to a unique set consisting of a GenerationController, a Generator and an ElementPreparator. These three classes work together to generate code in a specific fashion.

The GeneratorController controls the whole generation process. The most important method here is generateCode() which does the controlling work. There is a default implementation that handles working with UML models (UMLGeneratorController). The GeneratorController retrieves its corresponding Generator and starts its initialization.

The following sections provide a brief introduction on how to integrate a custom template into the code generation framework. Note that all the required Java classes as well as the templates and any other resources have to be part of a new plugin. In some cases, this introduction will refer to classes and methods of the plugin mechanism without mentioning the details. Please see the plugin manual for detailed instructions on how to create a new plugin for Poseidon for UML. A small example is used to clarify the parts where Java programming is necessary. For further details on the code generation API please refer to the API documentation.

The modification consists of four steps:

 

  1. Create a generator and register macro libraries

  2. Create a preparator and register working templates

  3. Create an element preparator

  4. Configure the code generation dialog


Subclassing UMLGenerator

In order to use self-written templates, a new instance of Generator with an overwritten init() method is needed. This method is used to set the Preparator used by the IntroGenerator, as well as all the known templates. Each template is mapped to a class of model elements. The default implementation (DefaultUMLGenerator) uses a default implementation of ElementPreparator (StaticElementPreparatorImpl) and the default templates (JavaClass.vm and JavaInterface.vm). Because we want to introduce our own template, this implementation is not sufficient. DefaultUMLGenerator has no other function than to specialize the initialization. Its superclass UMLGenerator is able to handle code generation for UML-based models, as long as the chosen model elements are subclasses of the UML class Classifier.

Because Classifier is the superclass for most of the static model elements representing nodes, it is sufficient for most needs and can be subclassed. UMLGenerator is an abstract class. Besides the init() method, there have to be implementations for generatePre() and generatePost() in non-abstract subclasses as well. These methods can be used to specifiy additional behavior between preparation and checking (generatePre()), between generation and compilation (generatePost()), or after compilation (compilePost()). For this example, we have used empty implementations.

..code_snippet..
package my.package;

import ru.novosoft.uml.foundation.core.MModelElement;
import com.gentleware.poseidon.generator.CodeTemplate;
import com.gentleware.poseidon.uml.generator.UMLGenerator;

public class IntroGenerator extends UMLGenerator {
public void init() {
super.init();
setPreparator(new IntroPreparator());
// create the template and associate it to use case
CodeTemplate template = new
CodeTemplate("/my/package/introTemplate.vm");
addCodeTemplate(MModelElement.class, template);
}

public void generatePre(String path) {
}

public void generatePost(String path) {
}

public void compilePost(String path) {
}
}
..code_snippet..

Note that we have set the Preparator in init(), and set our new template to be used for code generation for all model elements. Whenever the code generation finds a ModelElement, it will use the specified template.


Adding Macro Libraries

To add (or exchange) macro libraries, the plugin has to define a List of additional macro libraries. This list can then be added as an external macro library list to the Generator

..code_snippet..
Generator generator = CodeGenerationConnector.getGenerator("intro");
List macroList = new ArrayList();
macroList.add("my/templates/macro1.vm");
macroList.add("my/templates/macro2.vm");
CodeGenerationConnector.addExternalLibraries
(generator,"myplugin",macroList);
..code_snippet..

This example extends the Generator for the plugin "intro" and adds two new macro libraries to the list of external libraries. The new libraries are mapped to the name "myplugin", so that they can be removed later on using this name:

..code_snippet..
CodeGenerationConnector.removeExternalLibraries(generator,"myplugin");
..code_snippet..

The Generator has to know some rules in order to decide which external libraries to use. The Strategy pattern is used to define those rules. Any Generator instance can have an ExternalEntryStrategy set. The method useLibrariesFrom(String):boolean contains the rule implementation. It should return true if the libraries from the given plugin (addressed by the name you used when adding the libraries, so "myplugin" in this example) should be used. A custom implementation would need an implementation of ExternalEntryStrategy and an instance of the implementing class being set as the deciding strategy at the used Generator instance.

..code_snippet..
Generator generator = CodeGenerationConnector.getGenerator("intro");
generator.setExternalLibraryStrategy(new MyExternalEntryStrategy());
..code_snippet..

In order to decide whether its macros should be used, the strategy might want to inspect, e. g., the current GeneratorController, the Generator, the ElementPreparator, or the configuration settings. They are available via GeneratorFactory.getGeneratorController(), GeneratorController.getGenerator(), and Generator.getElementPreparator(), respectively. How to retrieve the configuration settings for code generation is described above (see the introduction to ).


Subclassing ElementPreparator

The initialization of IntroGenerator creates a new instance of the ElementPreparator to use. This example introduces its own ElementPreparator, called IntroPreparator. The default implementation (StaticElementPreparator) handles all elements of class diagrams. As long as code generation for classes, packages and interfaces is sufficient, the default implementation will do. The IntroPreparator introduced here adds handling of use cases to the code generation. So it is subclassed from the default implementation (StaticElementPreparatorImpl).

..code_snippet..
package my.package;

import ru.novosoft.uml.foundation.core.*;
import ru.novosoft.uml.behavior.use_cases.*;
import com.gentleware.poseidon.generator.PreparationException;
import com.gentleware.poseidon.uml.generator.*;

public class IntroPreparator extends StaticElementPreparatorImpl {

  // associate the prepared element class with model class use case
  public void init() {
    super.init();
    addPreparedElementClass(MUseCaseImpl.class,PreparedUseCase.class);
  }

  public PreparedModelElement prepare(MModelElement element)
  throws PreparationException {
    if (element == null)
      return null;

    // prepare a use case, or defer to the superclass
    PreparedModelElement preparedElement = null;
    if (element instanceof MUseCase)
      preparedElement = prepareUseCase((MUseCase)element);
    else
      preparedElement = super.prepare(element);

// associate the use case instance with this prepared element
if (preparedElement != null)
  addPreparedElement(element, preparedElement);
  return preparedElement;
}
public PreparedUseCase prepareUseCase(MUseCase usecase) {
PreparedUseCase preparedUseCase =
  (PreparedUseCase)getPreparedElementInstance(usecase.getClass());
preparedUseCase.init(usecase,this);
return preparedUseCase;
}
}
..code_snippet..

In order to add a new type of prepared element, a new ElementPreparator class has to be written. At least prepare() and init() have to be implemented. The init() method sets up the matching from UML classes to prepared objects. The prepare() method determines the cases in which the new element and its prepared class should be used. An instance of the preparation class is created on demand using getPreparedElementInstance(). Adding the new prepared element to the template context is handled automatically. Now the last task is to write the prepared element class for the use case.


Adding Prepared Element Classes

Adding new element preparation classes is very similar to adding new macro libraries. The set of element preparation classes is held in an ElementPreparator instance, wich will probably be the ElementPreparator that your code generation is based on. New preparation classes can be added as a list to the ElementPreparator instance.

..code_snippet..
Generator generator = CodeGenerationConnector.getGenerator("intro");
Map preparatorClasses = new HashMap();
preparatorClasses.put(MyA.class,PreparedMyA.class);
preparatorClasses.put(MyB.class,PreparedMyB.class);
CodeGenerationConnector.addExternalPreparationClasses(generator,
"myplugin", preparatorClasses);
..code_snippet..

As there is a unique mapping between the used Generator instance and the used ElementPreparator instance, getting the generator is sufficient. Anologous to the handling of the macro libraries, there has to be a strategy implementing the rules to decide what external preparation classes to use. This strategy has to be an implementation of ExternalEntryStrategy, too. The method usePreparationClassesFrom(String):boolean contains the rule implementation. The strategy class may be the same for the Generator and the ElementPreparator. Usually, the plugin providing the Generator and ElementPreparator classes should provide the strategy as well.

..code_snippet..
Generator generator =
CodeGenerationAdapterPlugin.getGenerator("intro");
Preparator prep = generator.getPreparator();
prep.setExternalPreparationClassStrategy(new
MyExternalEntryStrategy());
..code_snippet..

 


Writing Prepared Elements

You have to subclass PreparedModelElement for all UML prepared elements.

..code_snippet.. 
package my.package;

import ru.novosoft.uml.behavior.use_cases.*;
import com.gentleware.poseidon.uml.generator.*;

public class PreparedUseCase extends PreparedModelElement {
public void init(MUseCase usecase,
ModelElementPreparator preparator)
{
super.init(usecase,preparator);
}
}
..code_snippet..

This implementation of PreparedUseCase does not provide any really useful methods — for example methods to prepare the information provided by the use case. But that is not actually required. The existence of the PreparatorUseCase gives the template the chance to use the complete interface of MUseCase without knowing MUseCase itself. There is a method callElementMethod() in PreparedElement that is able to call any method from the public interface of the corresponding element.

A prepared element is a good place to prepare Strings for easy use in the template. For instance, you might want to list all participating actors for a use case. Doing this involves iterating over a list of all actors and retrieving their names. Within the template, this would require a lot of complicated code; in the prepared element, you have full access to the Java API, and so the loop is very simple. So you might have a method public String getInvolvedActors() that returns the required String, and the template just calls this method. You can take a look at Poseidon's own templates to get an idea of how this works. We use similar methods to create visibility modifiers, parameter signatures, and the like.

For this simple example, only three Java classes are needed to integrate our custom template into the code generation process. If elements of the class diagram had been sufficient for the template, only one Java class would have been necessary. The complexity of the template or the code generation goals will not affect the number of Java classes that much. As a rule of thumb, one Java class is needed when using only previously known model elements and two plus n (number of previously known Java classes) are needed otherwise.



Customizing the Code Generation Dialog

In order to present your plugin to the user, you have to have a radio button for your plugin in the code generation dialog (Generate Classes). If your extension provides a new language, you will want a new radio button; if it enhances the features of an existing language, you will want to have check boxes or radio buttons within that language. The generation system then finds out which plugin is enabled and hence, what is to be generated.

Also, you might have special settings for your plugin that the user might want to change. These might be for your plugin in general, or, if your plugin is applicable for several languages, you might have some special settings for HTML, some for Java, etc. This section shows you how to set all this, and how to retrieve the settings later on from your plugin.

Note that all settings in the dialog are automagically persistent — that is, the settings will be available when you close and restart Poseidon. Retrieval of the settings works like this:

// Create a key 
// Note: There are also methods with more or fewer String parameters
// for deeper and shallower nesting.
ConfigurationKey key = Configuration.makeKey("codeGen",
String item, String subitem);

// Retrieve a String setting
String setting = Configuration.getString(key);

// Retrieve a boolean setting
boolean setting = Configuration.getBoolean(key);

For all operations, you can use the class com.gentleware.poseidon.generator.ui.CodeGenerationConnector.


Adding a New Language

To add a new language, all you have to do is call CodeGenerationConnector.addLanguage(String language). The selected language is readable under the configuration key codeGen.language. It contains a lower-cased version of the language:

 

  • Java : java

  • C++ : cpp

  • PHP3 : php3

  • etc.

You can convert from upper case to lower case with the method CodeGenerationConnector.getLowercaseLanguage(String uppercaseLanguage).


Adding Features to a Language

For any language, you can add a subsetting. The following call can be used to enhance a special code generation:

CodeGenerationConnector.addLanguageFeature(String language,
String shortName, String longName, true)

The language is the button title, like Java. If you use a language that does not exist yet, a new radio button for that language will be created. The short name is the name under which you can retrieve the setting's value. It is the last part of the configuration key. The long name is the title of the radio button. So, if you called

addLanguageFeature("Java", "bold", "Bold reserved qualifiers", true)

you could read the key codeGen.java.bold and check if it is true or false. Inserting a radio button ensures that no other special feature for Java generation will be enabled. This will be the usual case, shown by giving the flag true as designating exclusive selection. You can also insert a check box, if you prefer, and the semantics of your generation plugin allows for that. The call is the same:

CodeGenerationConnector.addLanguageFeature( String language,
String shortName, String longName, false)

(For information on writing plugins that are not mutually exclusive, see Combining Multiple Extensions. )


Adding a Plugin Tab

All types of code generation may have special settings. For Java, you can set the compiler and the classpath for compilation. If you have the Statechart-to-Java plugin enabled, the settings dialog will show another tab “Statechart-to-Java”, with additional settings. If you have selected HTML, a different dialog will show up, with the settings tabs for HTML-related code generation.

Your plugin may also have settings that the user wants to change. You can create a panel (JPanel), add radio buttons, check boxes and text fields to it (JRadioButton, JCheckBox, JTextField), and then make this panel known to Poseidon. If you have set it up correctly, Poseidon will find the settings and store them so that they are persistent over subsequent runs of Poseidon.

There are two kinds of settings: plugin-specific (general settings) and language-specific (language settings). If your plugin provides enhancements for several languages, and each language enhancement requires different settings, you can create several language settings panels. For settings that are applicable to all supported languages, you can create another panel with general settings. They will be displayed regardless of the language selection.

Your new panel can have other panels nested in it, along with check boxes, radio buttons, and text fields. You can set the text of all these components to your liking, but you also have to give the components a name via setName(String name). The name is used to store the value of the field and to retrieve it later on, so that you can access the value of the settings entered by the user.

This is how you create a new panel:

  1. Create a JPanel.
  2. Create the JRadioButton, JCheckBox and JTextField components for your JPanel.
  3. Give each component a text and a name, and ...
  4. ... add the components to your JPanel.
  5. Finally, for language-specific settings, call
    CodeGenerationConnector.addSettingsPanel(String language,
    JPanel panel, String plugin)

If you have a JPanel with general settings for your plugin, and these settings are not language-specific, create another JPanel and call

CodeGenerationConnector.addSettingsPanel(JPanel panel, String plugin)

Let's say your plugin "ColorCode" provides color code generation for Java and HTML. You have decided that the user may set the color palette to be either "standard" or "colorful" (a general setting). For Java, the user can enable or disable colored Javadoc (a language setting), and for HTML, the user can enter an "author" text that will be displayed as the footer on every HTML page. This is the code that makes Poseidon show the settings:

JPanel general = new JPanel(); 
JCheckBox colorstyle = new JCheckBox("Use colorful schema");
colorstyle.setName("colorful");
general.add(colorstyle);
CodeGenerationConnector.addSettingsPanel(general, "ColorCode");

JPanel java = new JPanel();
JCheckBox colordoc = new JCheckBox("Colored Javadoc");
colordoc.setName("colordoc");
java.add(colordoc);
CodeGenerationConnector.addSettingsPanel("Java", java, "ColorCode");

JPanel html = new JPanel();
JTextField footer = new JTextField();
footer.setName("footer");
html.add(footer);
CodeGenerationConnector.addSettingsPanel("HTML", html, "ColorCode");

Note that the initial values of the settings are automatically retrieved if they are known from a previous run of Poseidon. The configuration key for a general setting will have the structure “codeGen.”+plugin+”.”+name, where name is the value you assigned by means of thesetName() method. A language-specific key will be available as "codeGen."+language+"."+plugin+"."+name.


Removing an Extension

All calls have their removal counterparts. If your plugin is uninstalled, you should remove the settings panels, the addition checkboxes, and the languages that you added, as described in the following list:

To remove a general settings panel:

CodeGenerationConnector.removeSettingsPanel(String plugin)

or

CodeGenerationConnector.removeSettingsPanel(JPanel panel)

To remove a special settings panel:

CodeGenerationConnector.removeSettingsPanel(String name, String plugin)

or

CodeGenerationConnector.removeSettingsPanel(JPanel panel)

To remove a language addition:

CodeGenerationConnector.removeLanguageFeature(String name, String shortName)
(where shortName is the same shortName you gave when installing the extension)

To remove a language:

First, remove all your extensions. Then, call

CodeGenerationConnector.removeLanguage(String language)


Replacing an Installed Plugin

Instead of adding a new plugin, you may want to modify or replace the code of an existing plugin. You can do this by overwriting the plugin JAR file (in the /plugins directory) with a JAR file containing the modified plugin code.


Resetting the Plugin Configuration

You can clear all the plugins from your plugin configuration by deleting the following files from your configuration directory:

  • $home/poseidon/SE/poseidon.inst
  • $home/poseidon/SE/installedModules.xml

The next time you start Poseidon, you will see the plugin configuration dialog. Here you can choose which plugins you want to reinstall.



© 2000 - 2010 Gentleware AG
         
                        
 support  documentation  documentation