MPS 2021.1 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 (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);

Gdc1

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

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

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

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

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

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

Gdc9

Gdc10

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.

Gdc11

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

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

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

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

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

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

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

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

Gdcb

  • 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

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

d

Gdc20

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


  • 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

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

  • 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

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

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

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

  • 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

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

  • 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

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

Last modified: 23 March 2021