Generator User Guide Demo3
Generator User Guide Demo 3
In Demo 2 we were generating java statements like:
to create a Swing component and add it to the application's content pane.
In Demo 3 we are going to add support for component properties, which will require generation of more complex initialization code - not just a constructor invocation. Moreover, the generated property initialization code is going to be different for different types of components. Therefore we will choose a generation strategy that is capable of handling such requirements.
New Language
Again, we need to setup a new language and copy some of the Demo 2 generator artifacts to it.
create a new language: 'generator_demo.demoLang3'
in the language properties dialog add an extended dependency on 'jetbrains.mps.sampleXML' as well as on 'jetbrains.mps.baseLanguage'
create a new generator for this language, if it does not exist (see Generator User Guide Demo1 for details)
delete the mapping configuration 'main' from the demoLang3 generator (as in Demo 2, we will copy all needed parts from the demoLang2 generator to the demoLang3 generator)
copy-paste the mapping configuration 'main' from the demoLang2 generator to the demoLang3 generator
copy-paste the 'DemoApp' template from the demoLang2 generator to the demoLang3 generator
addContent(container)
The generator will resemble in many aspects the one we created in Demo 2. So we'll be using the conditional root rule to insert the DemoApp class into the output model. Initially, we need to make some tweaks to the DemoApp class.
open the 'DemoApp' template
add a static void method 'addContent(Container)'
in the 'main()' method find the statement:
$LOOP$[container.add($SWITCH$[null]);replace the statement above with the statement below:
addContent(container);
Now, the addContent() method is supposed to add all components to the JFrame. So it will call container.add() and pass in initialized visual components in turn - one for each input XML Document. So we'll add a dummy method that we can use in the code and have the generator replace it with real component initialization code.
Generating components
In order to have a component initialization method generated for each input Element, we will wrap the component() method declaration with a LOOP macro. The LOOP macro will iterate over all Elements in the input model and generate a method for each of them.
Similarly, we need to call these generated methods for each input Element and add the returned components into the container, so we need to use another LOOP macro around the call to the container.add(component()); method inside the addContent() method.
Last but not the least, we have to replace the call to the dummy component() method with a call to the real generated method corresponding to the same input Element. So select component (without the ending parentheses) and insert a reference macro.
This is where we can't move forward just yet. We have no clue how to resolve the method declaration corresponding to the current node so that we could refer to it from the reference macro. We will employ mapping labels at this stage. They will serve as registries for method declarations, accessible by the Element that they were created for. So our reference macro will be able to retrieve the corresponding method declaration from there.
To define a mapping label, go back to the 'main' mapping configuration:
Now, back to DempApp, we can finish the reference macro:
Obviously now we need to complete the generation of method declarations and properly store them in the method mapping label.
Inserting components
Just like in Demo 2, we'll use a SWITCH macro to accommodate for different types of Elements when generating the methods for them. So right-click on the generator model and choose New -> template switch. Depending on the name of the Element, we either generate code for a button or for a label.
This time, since the generated code is more complex, in-line templates would not be enough to capture the logic, so we need standalone templates for both generating a button and a label - named insert_Button and insert_Label respectively. Create them by right-clicking on the generator model and choosing New -> template declaration.
Templates are snippets of the code to generate, parametrized with values calculated from the input model. In our case, we need to generate a new static method that will be added to the DemoApp class and will have a signature compatible with the component() method declared in the DemoApp class template. So what we'll do now for a button is to create a dummy class with a static method in it. This static method will be made a template fragment, so that only the method and not the whole class will be inserted into the target model.
First, choose ClassConcept as the content node:
Enter the code below declaring the static method with the signature that we want.
Now select the whole method declaration, hit Alt+Enter and pick Create Template Fragment from the menu. This will indicate that it is only our method declaration that should be used as a template. The surrounding class serves merely as context for the method.
This is the time when we register all methods generated with this template into the method mapping label, so that we can retrieve these later when generating calls to these methods. Simply enter the desired mapping label in the Inspector window for the template fragment (the <TF visual element).
At the moment we are generating methods for all input Elements with identical name - createComponent, which would cause Java compile-time troubles, if we have more than one Element in the input model. We need to give each generated method a unique name and we'll do it through a property macro.
We're using the genContext capabilities to generate unique identifiers. The templateValue parameter is going to be "createComponent" (i.e. the method's name as written in template).
Now, please, create template declaration for creating a JLabel.
Once done, we should be able to complete the SWITCH template declaration.
After fixing the SWITCH macro in the DempApp class template and making the language, we should be able to give our new generator a first try.
New Test Model
Let's create a new input test model:
go to the 'test_models' solution
clone the model 'test2' to model 'test3'
in the model properties dialog replace 'engaged on generation' language demoLang2 -> demoLang3 (see Generator User Guide Demo2 for details)
open the 'Button' document (from model 'test3') in the editor
add an attribute 'text="Hello"' to the 'button' element
add an attribute 'enabled="false"' to the 'button' element
add an attribute 'text="world!"' to the 'label' element in the 'Label' document
add an attribute 'background="orange"' to the 'label' element in the 'Label' document
When you hit Preview generated code, you should get a valid Java application.
Notice the uniqueness of the createComponent() methods.
Now, let's define the semantics for the extra XML attributes.
Adding an $IF$ macro
Let's focus on the insert_Button and insert_Label templates. These define the code that will initialize the Swing components based on the values specified in the input XML Element. So far we ignore all the attributes in the input model, but this will change now. Focusing on insert_Label for now, if the input Element has an attribute called 'text', we will generate a statement:
where text is the string specified in the 'text' attribute of the input Element.
at the top specify Element as the input of the template so that MPS can type-check code involving the template's current node
enter the following code in the body of the 'createComponent()' method
create IF-macro around the 'component.setText("text");' statement (select whole statement, press Ctrl-Shift+M)
enter the code into the condition function of IF-macro, which will check the presence of the 'text' attribute
create property-macro inside the string literal "text", because we need to parametrize the value with the actual attribute value from the input model
in the value function of the property-macro enter code that returns the value of the 'text' attribute of the input Element
The same steps should now be repeated to the insert_Button template.
Reusable template
We can go on adding in the same manner support for more and more component properties, but this way we are going to end up with a lot of duplicated code in our templates.
A better idea is to create one template containing some common code and re-use it in template of each component. For example, let's add support for the property 'enabled'. We'll create a "shared" template and re-use it in other templates using the CALL macro:
go to the 'main@generator' model in demoLang3 generator (select in project tree)
create new template declaration node using Create Root Node in model popup menu
name it 'include_ComponentProperties'
choose input - Element
choose StatementList as the template's content node:
in the statement list create a variable declaration:
JComponent component = null;add a block-statement (press <Enter> after the variable declaration, type '{', press Ctrl+<space> to auto-complete)
create a statement:
component.setEnabled(false);inside the block-statement
mark the block-statement (i.e. excluding the declaration of the component variable) as a template fragment:
MAP_SRC-macro
As Generator User Guide Demo3 of the 'text' property, the 'setEnabled()' method call generation should be conditional - this statement should only be generated if the input element posses the attribute 'enabled'.
This time, to achieve the conditional generation, instead of an IF-macro we will use a MAP_SRC-macro. As we will see, MAP_SRC-macros have several advantages over IF-macros in this case.
create a MAP_SRC-macro (it should wrap the whole statement )
enter code in the macro's mapped node function as shown:
The mapped node function returns the node for the 'enabled' attribute, if present, and null otherwise. If the mapped node function in a MAP_SRC-macro returns null, the generator then ignores the node wrapped by this macro (just like an If macro would).
However, if the mapped node is not null, then it becomes the current input node, while processing the wrapped template code. This helps us get hold of the attribute making creation of the property-macro a bit easier compared to using an IF-macro - we don't have to find the 'enabled' attribute again, this attribute is already our input node.
Now, to set the correct value to the template:
attach a property-macro to the boolean constant 'false'
enter the code into its value function (note that this time the value function expects a boolean return value):
The CALL-macro
We will use the CALL macro to specify places in code, into which we want to insert the 'component.setEnabled(..);' statement during generation.
open the 'insert_Button' template in the editor
insert a new empty line just after the statement
component.setText("text");insert the CALL node macro on the empty line
make a reference to the 'include_ComponentProperties' template in the macro's inspector:
create a similar macro in the 'insert_Label' template
re-generate generator model (Shift+F9)
Testing what we have
Generating code for the 'test3' model will render a valid Java application:
We correctly get the setEnabled() method generated only for the button, but not for the label.
Reference-macro (Resolving by Name)
In many cases reference cannot be resolved automatically and this is where reference-macros come in handy.
For example, let's add support for the 'background' property into our generator:
open the 'include_ComponentProperties' template
add a block-statement just after the 'component.setEnabled(false);' statement
enter the following code inside that block-statement:
component.setOpaque(true); component.setBackground(Color.black);wrap the block-statement in a MAP_SRC-macro
enter the code into the macro's mapped node function as shown:
attach a reference-macro to the static field reference 'black' (in 'Color.black' expression), since we need to parametrize the color depending on the actual value of the 'background' attribute of the input XML Element.
enter the following code inside the referent function:
In this example, the referent function has combined return type:
having the eithor-or semantics. That means that we have two alternatives for what to return from the function:
we can find a suitable static field declaration in class 'Color' and return that node
or we can return a reference info string - a name of static field declaration (name of color) and let it up to MPS to find the static field for us
The 2nd option, of course, is much more attractive. Thus we are returning a value of the 'background' attribute - the name of the desired color.
The final test
Now, after a language rebuild the 'test3' model will be generated into a Java application that now takes into account all the input element's attributes.