domain specific language creation using jetbrains mps - part 3
In the previous post (see here), we created a simple DSL to control a printing machine. Our customer is super happy about the fact that they can initialize and control their brand new printer using our DSL. However, there is still one requirement that we need to fulfill: Our customer wants to be able to use only specific printing units at a time and not all of them. In this sense, they can print pens for the TU/e using only parts of the logo.
In order to meet this requirement, we are going to introduce a new concept in our design. This new concept will be named “Recipe” and will contain the information about the printing units that will be used. After we define the recipe, we are going to export it in an XML file, which will be read from our Main class, which will initialize the printer accordingly. We’re going to split this process in two tasks. The first task has to do with the update of our design on Java’s part, and the second task is to update our DSL design as well, to make it consistent with our Java design.
We will introduce the Recipe class in our design. This class will be introduced with the relation “a printer has a recipe”. The recipe class itself is going to be nothing more but a string. The characters contained in this string will specify which printing units will operate on products. We can write the recipe class as:
package me.eparon.machine;
public class Recipe {
private String characters;
public Recipe(String chars) {
characters=chars;
}
public String getCharacters(){
return characters;
}
}
Now, as we said, each printing unit should print its character if and only if this character is contained in the recipe’s string. We need to make the following adjustments to the printing unit class:
package me.eparon.machine;
import me.eparon.product.*;
public class PrintingUnit {
protected char part;
protected Product product;
protected Recipe recipe;
public void initialize(char p, Recipe r) {
part = p;
recipe =r;
product = null;
}
public void setProduct(Product p) {
product = p;
}
public void printProduct() {
if( product != null ) {
if (recipe.getCharacters().indexOf(part)>=0) {
product.addItem(part);
}
}
}
}
Next, we need to update the Main class as well:
package me.eparon;
import java.util.List;
import java.util.ArrayList;
import me.eparon.machine.*;
import me.eparon.product.*;
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<Product>();
for (int i = 0; i < 24; i++) {
products.add(new Product());
}
String ingredients = "ab";
Recipe recipe = new Recipe(ingredients);
PrintingUnit unit1 = new PrintingUnit();
PrintingUnit unit2 = new PrintingUnit();
PrintingUnit unit3 = new PrintingUnit();
unit1.initialize('a', recipe);
unit2.initialize('b', recipe);
unit3.initialize('c', recipe);
Printer printer = new Printer();
printer.addPrintingUnit(unit1);
printer.addPrintingUnit(unit2);
printer.addPrintingUnit(unit3);
printer.initializeProduction(products);
System.out.println("results");
printer.performPrinting();
}
}
If we run the above-mentioned code, we will get the following results:
results
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
ab
Process finished with exit code 0
As we expected, only two from the three printing units operated on the products and the third printing unit did not take part in the process. Now we’re ready to see how we can proceed into changing the DSL part.
Before proceeding with the technical details of our DSL, let’s think about how do we want to define our recipe. A recipe runs on a machine, has a name, and also has some characters. For example, we could use the following concept to define our recipe:
recipe: test_recipe runs on: test_printer allows: abc
In this case, our recipe runs on the machine test_printer and allows a, b, and c printing units to work.
After defining our recipe like this, we want to create an XML file that contains these information. This XML file will be read by the Main class and the printing units are going to be informed about the recipe. The structure of this file, is:
<Recipe>
<Name>test</Name>
<Machine>blah</Machine>
<Ingredients>abc</Ingredients>
</Recipe>
As you would guess, this time we will need a different kind of code generator – one that will generate XML code for us.
Let’s see how we can achieve this.
The first step is going to be the creation of our new concept, “Recipe”. This is going to be a root concept that has a string property (ingredients), a reference to one printer, and will implement the INamedConcept interface:
concept Recipe extends BaseConcept
implements INamedConcept
instance can be root: true
alias: <no alias>
short description: <no short description>
properties:
ingredients : string
children:
<< ... >>
references:
printer : Printer[1]
After defining our concept, we need to modify its editor in order to make sure that it appears exactly as we want it to appear.
<default> editor for concept Recipe
node cell layout:
[- recipe: { name } runs on: ( % printer % -> { name } ) allows: { ingredients } -]
inspected cell layout:
<choose cell model>
Next, we can create a new sandbox for this concept and see if it looks the way we want:
recipe: test_recipe runs on: test allows: abc
Right now, we have only two steps left: creating the XML generator, and creating a constraint that will prevent us from putting ingredients in our recipe that are not supported from our printing units. Let’s consider an example: We have three printing units: a, b, and c. In our recipe we should not be able to define as ingredient the character e or the character o.
Let’s see how we can create the constraint we described before.
Open the Recipe concept, and then go to the tab “Constraints.” Click to create a new constraint. This is going to be a constraint for the property “ingredients” of this concept, so this should be defined in the << property constraints >>
section. Put your cursor in this section, hit enter, and then define the property name (ingredients). Here we get three options on which we can set constraints. For this specific example, we are only interested in the is valid
constraint, as we want to check the validity of the recipe’s ingredients.
If we think our constraint as an algorithm, we want to get all the characters of our recipe and check if there’s a production unit configured with this character. If this is the case, then we accept the character, otherwise the recipe is not valid:
concepts constraints Recipe {
can be child <none>
can be root <none>
can be parent <none>
can be ancestor <none>
property {ingredients}
get:<default>
set:<default>
is valid:(propertyValue, node)->boolean {
boolean match = true;
for (char s : propertyValue.toCharArray()) {
if (node.printer.listOfPrintingUnits.any({~it => it.prints.indexOf(s) >= 0; }) == false) {
match = false;
break;
}
}
return match;
}
<<referent constraints>>
default scope
<no default scope>
}
Now, if we rebuild our language and go to the recipe we defined in our sandbox, we can check if our constraint is working. For instance, defining a recipe like:
recipe: test_recipe runs on: test allows: abcg
The last part of the recipe should be marked red, indicating that there is an error.
Let’s create the generator of the XML code. In order to do that, we first need to add to our generator the reference to the XML libraries of MPS. This can be done with the following steps: Right-click on the “main@generator” and select “Module properties.” Next, go to “Used Languages” and add the language “jetbrains.mps.core.xml”. Rebuild your generator.
Now, go to your “main” item inside your generator and add a new “Root Mapping Rule”. Choose “Recipe” as concept and move your cursor to “no template”. Hit Alt+Enter, choose “New Root Template” and then “xml file.” If you want, you can rename the mapping file to “Recipe”, instead of “map_Recipe.”
The XML generator we will build is a very simple one and looks like:
xml Recipe
<no prolog>
<Recipe>
<Name>$[name]</Name>
<Printer>$[printer]</Printer>
<Ingredients>$[ingr]</Ingredients>
</Recipe>
In this case, we use the following notation:
$[name]
, is a node property, which gets the node.name value.$[printer]
, is a node property, which gets the node.printer.name value.$[name]
, is a node property, which gets the node.ingredients value.Now if we try to generate text from our sandbox, we will get:
<Recipe>
<Name>test_recipe</Name>
<Printer>test</Printer>
<Ingredients>abc</Ingredients>
</Recipe>
Let’s save this file as “recipe.xml” on our disk.
The only thing that’s left now, is to modify our Main class in such a way that it will be possible to read the XML file and load the recipe to our printer.
The Main class, needs to become like:
package me.eparon;
import java.util.List;
import java.util.ArrayList;
import java.io.File;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import org.w3c.dom.Document;
import me.eparon.machine.*;
import me.eparon.product.*;
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<Product>();
for (int i = 0; i < 24; i++) {
products.add(new Product());
}
File xmlRecipe = new File("recipe.xml");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
String ingredients = "";
try {
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(xmlRecipe);
ingredients = doc.getElementsByTagName("Ingredients").item(0).getTextContent();
} catch (Exception ex) {
System.out.println("Huston we have a problem.");
}
Recipe recipe = new Recipe(ingredients);
PrintingUnit unit1 = new PrintingUnit();
PrintingUnit unit2 = new PrintingUnit();
PrintingUnit unit3 = new PrintingUnit();
unit1.initialize('a', recipe);
unit2.initialize('b', recipe);
unit3.initialize('c', recipe);
Printer printer = new Printer();
printer.addPrintingUnit(unit1);
printer.addPrintingUnit(unit2);
printer.addPrintingUnit(unit3);
printer.initializeProduction(products);
System.out.println("results");
printer.performPrinting();
}
}
The new lines we’ve added read the XML file “recipe.xml”, and load the ingredients contained there.
As a last thing, we need to put those changes in our Java code generator file, without forgetting to add as reference to our generator the following: “java.xml.parsers@java_stub”, “java.io@java_stub”, “org.w3c.dom@java_stub”.
Finally, this generator becomes:
public class Main {
public static void main(String[] args) {
List<Product> products = new ArrayList<Product>();
int noOfProd = $0;
for (int i = 0; i < noOfProd; i++) {
products.add(new Product());
}
File xmlRecipe = new File("recipe.xml");
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
String ingredients = "";
try {
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(xmlRecipe);
ingredients = doc.getElementsByTagName("Ingredients").item(0).getTextContent();
} catch (Exception ex) {
System.out.println("Huston we have a problem.");
}
Recipe recipe = new Recipe(ingredients);
$LOOP$[PrintingUnit $[unit] = new PrintingUnit();]
$LOOP$[->$[unit].initialize('$[c]', recipe);]
Printer printer = new Printer();
$LOOP$[printer.addPrintingUnit(->$[unit]);]
printer.initializeProduction(products);
System.out.println("results");
printer.performPrinting();
}
}
And at this point our exploration of the DSL world comes to an end. I hope you enjoyed reading this series of posts!