MPS 2023.3 Help

Generator User Guide Demo3

Generator User Guide Demo 3

In Demo 2 we were generating java statements like:

container.add(new JButton());

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. For more information, refer to Generator User Guide Demo1

  • 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);
gdc1.png

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.

gdc3.png

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.

gdc4.png

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.

gdc5.png

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.

gdc6.png

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:

gdc7.png

Now, back to DempApp, we can finish the reference macro:

gdc9.png
gdc10.png

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 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.

gdc11.png

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:

gdc12.png

Enter the code below declaring the static method with the signature that we want.

gdc13.png

Now select the whole method declaration, press Alt+Enter  and select 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.

gdc14.png

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).

gdc15.png

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.

gdc16.png

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.

gdc18.png

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.

gdc19.png

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. For more information, refer to Generator User Guide Demo2

  • 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

gdcb.png
  • 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

gdcl.png

When you hit Preview generated code, you should get a valid Java application.

gdc20.png

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:

component.setText( _text_ )

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

gdc21.png
  • 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

gdc22.png

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:

  • gdc23.png
  • 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:

  • gdc24.png

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:

gdc25.png

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):

gdc26.png

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:

  • gdc27.png
  • 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:

gdc28.png

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:

gdc29.png
  • 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:

gdc30.png

In this example, the referent function has combined return type:

JOIN(node<StaticFieldDeclaration> | String)

having the eithor-or semantics. That means that we have two alternatives for what to return from the function:

  1. we can find a suitable static field declaration in class 'Color' and return that node

  2. 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.

gdc31.png
Last modified: 07 March 2024