MiniJDL brief
The capstone asks you to design a small language for applications with entities, fields and directed relations. From one model, you will check domain rules, generate SQL DDL, Java data classes and HTML documentation, then transform the entities into a relational model. This MiniJDL is a teaching language, not JHipster’s JDL, and its generated Java classes are simple data declarations rather than a working web application.
The idea of generating several coordinated artefacts from an entity description is adapted from Dimitris Kolovos, Minimal JHipster JDL Monolith Example (2021). The MiniJDL metamodel, shop model and Epsilon programs here are written for this tutorial. You can use the Module 10 bundle locally or open each example in the Playground.
Aim for a repeatable pipeline: a metamodel defines the language; a model records the application; EVL rejects bad data; EGL/EGX generate readable artefacts; ETL creates another model with explicit mappings. Each stage gives you evidence to inspect before trusting the next one.
Define the language
Open the first example’s minijdl.emf. An Application contains Entity objects; an Entity contains Field and Relation objects. A field has a FieldType of STRING, INTEGER or BOOLEAN. A relation references a target entity. val makes ownership clear; ref means the relation points to an existing entity rather than owning another copy.
The Flexmi model describes Shop: Customer has email and active, Product has title and price, and Purchase has quantity plus relations to the other two entities. The EOL query prints the counts so you can check that the model loaded as intended.
var app = Application.all.first();
("Application: " + app.name).println();
for (e in app.entities) {
var fieldNoun = "fields";
if (e.fields.size() = 1) { fieldNoun = "field"; }
var relationNoun = "relations";
if (e.relations.size() = 1) { relationNoun = "relation"; }
(e.name + ": " + e.fields.size() + " " + fieldNoun + ", " + e.relations.size() + " " + relationNoun).println();
}
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
attr String[1] name;
val Entity[*] entities;
}
class Entity {
attr String[1] name;
val Field[*] fields;
val Relation[*] relations;
}
class Field {
attr String[1] name;
attr FieldType type;
}
class Relation {
attr String[1] name;
ref Entity target;
}
<?nsuri minijdl?>
<application name="Shop">
<entity name="Customer">
<field name="email" type="STRING"/>
<field name="active" type="BOOLEAN"/>
</entity>
<entity name="Product">
<field name="title" type="STRING"/>
<field name="price" type="INTEGER"/>
</entity>
<entity name="Purchase">
<field name="quantity" type="INTEGER"/>
<relation name="customer" target="Shop.Customer"/>
<relation name="product" target="Shop.Product"/>
</entity>
</application>
Application: Shop
Customer: 2 fields, 0 relations
Product: 2 fields, 0 relations
Purchase: 1 field, 2 relations
The metamodel captures structure, not every business rule. It does not, for example, prevent two fields in one entity from sharing a name. That rule belongs in validation. Adding a new field type later would require updating both the metamodel enum and every generator’s type mapping.
Build and validate a model
The next model deliberately has two Customer.email fields. EVL checks uniqueness of entity names and of field names within each entity, and checks that each relation has a target. The duplicate field produces two findings because the constraint runs on both offending field elements. This is a validation result, not a parser failure.
context Entity {
constraint UniqueEntityName {
check: Entity.all.select(e | e.name = self.name).size() = 1
message: "Duplicate entity: " + self.name
}
}
context Field {
constraint UniqueFieldName {
check: self.eContainer().fields.select(f | f.name = self.name).size() = 1
message: "Duplicate field: " + self.eContainer().name + "." + self.name
}
}
context Relation {
constraint HasTarget {
check: not self.target.isUndefined()
message: "Missing relation target: " + self.name
}
}
More files: shop.flexmi, minijdl.emf
<?nsuri minijdl?>
<application name="Shop">
<entity name="Customer">
<field name="email" type="STRING"/>
<field name="email" type="BOOLEAN"/>
</entity>
<entity name="Product">
<field name="title" type="STRING"/>
<field name="price" type="INTEGER"/>
</entity>
<entity name="Purchase">
<field name="quantity" type="INTEGER"/>
<relation name="customer" target="Shop.Customer"/>
<relation name="product" target="Shop.Product"/>
</entity>
</application>
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
attr String[1] name;
val Entity[*] entities;
}
class Entity {
attr String[1] name;
val Field[*] fields;
val Relation[*] relations;
}
class Field {
attr String[1] name;
attr FieldType type;
}
class Relation {
attr String[1] name;
ref Entity target;
}
Error [UniqueFieldName]: Duplicate field: Customer.email
Error [UniqueFieldName]: Duplicate field: Customer.email
Repair the duplicate before generating deliverables. The worked solution in Exercise 4 runs the same rules on a corrected model. In a real project, add checks for identifier syntax, names reserved by target languages, relation-name clashes, required fields and cycles where relevant. A passing result from this small rule set does not imply those extra policies.
Generate SQL, Java and HTML
EGL maps the shop model to SQL DDL. Each entity becomes a table with an id column; fields map to SQL column types; relations become _id columns and foreign-key declarations. Inspect the generated SQL and verify that Purchase refers to Customer and Product by name. This template targets a simple, generic SQL dialect; database-specific types and naming rules would need a deliberate choice in a production generator.
[%
var ddl = "-- MiniJDL schema for " + Application.all.first().name + "\n";
for (e in Entity.all) {
ddl += "CREATE TABLE " + e.name + " (\n id INTEGER PRIMARY KEY";
for (f in e.fields) {
var sqlType = "INTEGER";
if (f.type.toString() = "STRING") { sqlType = "VARCHAR(255)"; }
else if (f.type.toString() = "BOOLEAN") { sqlType = "BOOLEAN"; }
ddl += ",\n " + f.name + " " + sqlType;
}
for (r in e.relations) {
ddl += ",\n " + r.name + "_id INTEGER";
}
for (r in e.relations) {
ddl += ",\n FOREIGN KEY (" + r.name + "_id) REFERENCES " + r.target.name + "(id)";
}
ddl += "\n);\n";
}
%][%=ddl%]
More files: shop.flexmi, minijdl.emf
<?nsuri minijdl?>
<application name="Shop">
<entity name="Customer">
<field name="email" type="STRING"/>
<field name="active" type="BOOLEAN"/>
</entity>
<entity name="Product">
<field name="title" type="STRING"/>
<field name="price" type="INTEGER"/>
</entity>
<entity name="Purchase">
<field name="quantity" type="INTEGER"/>
<relation name="customer" target="Shop.Customer"/>
<relation name="product" target="Shop.Product"/>
</entity>
</application>
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
attr String[1] name;
val Entity[*] entities;
}
class Entity {
attr String[1] name;
val Field[*] fields;
val Relation[*] relations;
}
class Field {
attr String[1] name;
attr FieldType type;
}
class Relation {
attr String[1] name;
ref Entity target;
}
-- MiniJDL schema for Shop
CREATE TABLE Customer (
id INTEGER PRIMARY KEY,
email VARCHAR(255),
active BOOLEAN
);
CREATE TABLE Product (
id INTEGER PRIMARY KEY,
title VARCHAR(255),
price INTEGER
);
CREATE TABLE Purchase (
id INTEGER PRIMARY KEY,
quantity INTEGER,
customer_id INTEGER,
product_id INTEGER,
FOREIGN KEY (customer_id) REFERENCES Customer(id),
FOREIGN KEY (product_id) REFERENCES Product(id)
);
EGX invokes an EGL template once per entity, producing Customer.java, Product.java and Purchase.java. The template maps field types to Java types and relations to typed references. These files illustrate generation; they are not compiled by the Playground, and relationship persistence is outside this small example.
rule Entity2Java
transform e : Entity {
template: "template.egl"
target: e.name + ".java"
}
public class [%=e.name%] {
public int id;
[% for (f in e.fields) { %] public [% if (f.type.toString() = "STRING") { %]String[% } else if (f.type.toString() = "BOOLEAN") { %]boolean[% } else { %]int[% } %] [%=f.name%];
[% } %][% for (r in e.relations) { %] public [%=r.target.name%] [%=r.name%];
[% } %]}
More files: shop.flexmi, minijdl.emf
<?nsuri minijdl?>
<application name="Shop">
<entity name="Customer">
<field name="email" type="STRING"/>
<field name="active" type="BOOLEAN"/>
</entity>
<entity name="Product">
<field name="title" type="STRING"/>
<field name="price" type="INTEGER"/>
</entity>
<entity name="Purchase">
<field name="quantity" type="INTEGER"/>
<relation name="customer" target="Shop.Customer"/>
<relation name="product" target="Shop.Product"/>
</entity>
</application>
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
attr String[1] name;
val Entity[*] entities;
}
class Entity {
attr String[1] name;
val Field[*] fields;
val Relation[*] relations;
}
class Field {
attr String[1] name;
attr FieldType type;
}
class Relation {
attr String[1] name;
ref Entity target;
}
--- Customer.java ---
public class Customer {
public int id;
public String email;
public boolean active;
}
--- Product.java ---
public class Product {
public int id;
public String title;
public int price;
}
--- Purchase.java ---
public class Purchase {
public int id;
public int quantity;
public Customer customer;
public Product product;
}
The HTML template provides a third view of the same model. It lists entities, fields and relation counts. Open its generated output and compare the names with the SQL and Java outputs. If one generator omits an entity, the common model makes the mismatch detectable.
<h1>[%=Application.all.first().name%] entities</h1>
[% for (e in Entity.all) { %]<section><h2>[%=e.name%]</h2><ul>
[% for (f in e.fields) { %]<li>[%=f.name%]: [%=f.type%]</li>
[% } %]</ul><p>Relations: [%=e.relations.size()%]</p></section>
[% } %]
More files: shop.flexmi, minijdl.emf
<?nsuri minijdl?>
<application name="Shop">
<entity name="Customer">
<field name="email" type="STRING"/>
<field name="active" type="BOOLEAN"/>
</entity>
<entity name="Product">
<field name="title" type="STRING"/>
<field name="price" type="INTEGER"/>
</entity>
<entity name="Purchase">
<field name="quantity" type="INTEGER"/>
<relation name="customer" target="Shop.Customer"/>
<relation name="product" target="Shop.Product"/>
</entity>
</application>
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
attr String[1] name;
val Entity[*] entities;
}
class Entity {
attr String[1] name;
val Field[*] fields;
val Relation[*] relations;
}
class Field {
attr String[1] name;
attr FieldType type;
}
class Relation {
attr String[1] name;
ref Entity target;
}
<h1>Shop entities</h1>
<section><h2>Customer</h2><ul>
<li>email: STRING</li>
<li>active: BOOLEAN</li>
</ul><p>Relations: 0</p></section>
<section><h2>Product</h2><ul>
<li>title: STRING</li>
<li>price: INTEGER</li>
</ul><p>Relations: 0</p></section>
<section><h2>Purchase</h2><ul>
<li>quantity: INTEGER</li>
</ul><p>Relations: 2</p></section>
Transform to a relational model
Text generation is not the only route. The ETL program maps Application to Schema, Entity to Table, and Field to Column. The target conforms to its own relational.emf metamodel, which can be validated or consumed by another generator. The initial mapping leaves relations out deliberately so you can see what information is lost.
rule Application2Schema
transform a : Source!Application
to s : Target!Schema {
s.name = a.name;
s.tables ::= a.entities;
}
rule Entity2Table
transform e : Source!Entity
to t : Target!Table {
t.name = e.name;
var id = new Target!Column;
id.name = "id";
id.sqlType = "INTEGER";
t.columns ::= e.fields;
t.columns.add(id);
}
rule Field2Column
transform f : Source!Field
to c : Target!Column {
c.name = f.name;
if (f.type.toString() = "STRING") { c.sqlType = "VARCHAR(255)"; }
else if (f.type.toString() = "BOOLEAN") { c.sqlType = "BOOLEAN"; }
else { c.sqlType = "INTEGER"; }
}
@namespace(uri="relational", prefix="relational")
package relational;
class Schema {
attr String name;
val Table[*] tables;
}
class Table {
attr String name;
val Column[*] columns;
val ForeignKey[*] foreignKeys;
}
class Column {
attr String name;
attr String sqlType;
}
class ForeignKey {
attr String name;
attr String column;
ref Table target;
}
More files: shop.flexmi, minijdl.emf
<?nsuri minijdl?>
<application name="Shop">
<entity name="Customer">
<field name="email" type="STRING"/>
<field name="active" type="BOOLEAN"/>
</entity>
<entity name="Product">
<field name="title" type="STRING"/>
<field name="price" type="INTEGER"/>
</entity>
<entity name="Purchase">
<field name="quantity" type="INTEGER"/>
<relation name="customer" target="Shop.Customer"/>
<relation name="product" target="Shop.Product"/>
</entity>
</application>
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
attr String[1] name;
val Entity[*] entities;
}
class Entity {
attr String[1] name;
val Field[*] fields;
val Relation[*] relations;
}
class Field {
attr String[1] name;
attr FieldType type;
}
class Relation {
attr String[1] name;
ref Entity target;
}
Schema name="Shop"
Table name="Customer"
Column name="email" sqlType="VARCHAR(255)"
Column name="active" sqlType="BOOLEAN"
Column name="id" sqlType="INTEGER"
Table name="Product"
Column name="title" sqlType="VARCHAR(255)"
Column name="price" sqlType="INTEGER"
Column name="id" sqlType="INTEGER"
Table name="Purchase"
Column name="quantity" sqlType="INTEGER"
Column name="id" sqlType="INTEGER"
Compare the target tree with the source: three tables have five declared-field columns and three generated id columns, but no foreign keys. The ETL rule adds id after t.columns ::= e.fields, because that assignment replaces the earlier column collection. Exercise 6 adds relation key columns and a Relation2ForeignKey rule, attaching its products to the right table. A successful transformation run does not mean the mapping is complete; decide the target invariants before calling it done.
Assess and extend the solution
Use this checklist on your own MiniJDL variant:
- Language: Does every class, containment and reference have a purpose? Is each model element owned exactly where intended?
- Model: Do all named references resolve? Can a second, different application be written without changing the metamodel?
- Validation: Do invalid names and relations produce clear findings? Does the corrected model pass? Which rules remain unimplemented?
- SQL: Are all entities, fields and relation keys present? Is the output valid for the database you choose?
- Java: Are the expected files and types present? If you need compilable code, have you compiled it and addressed packages/imports?
- HTML: Does the documentation describe the same entities and relations as the model?
- ETL: Do table and column counts match? Does each foreign key point to the intended target table? Can the target be validated?
- Repeatability: Can you rerun the pipeline after a model edit and compare the outputs without manual patching?
The reference examples give a small end-to-end solution. They do not generate a complete JHipster application. Extending MiniJDL with multiplicities, constraints, application configuration and persistence choices would be a new design exercise.
Exercises
Trace ownership. Why does an Entity contain its Field objects, while Relation.target is a reference?
Show solution
Fields belong to the entity that declares them. A relation points at another entity already owned by the application; containing that target would duplicate or move it.
Trace one element. Follow Purchase.customer through the SQL, Java and HTML examples. Which representation carries the target name, and what does the HTML example omit?
Show solution
SQL declares customer_id with a foreign key to Customer; Java declares a Customer customer field. The HTML example reports two relations for Purchase but does not list their names or targets, so it is a summary rather than a full schema specification.
Find information loss. What does the first relational ETL target omit, and why should that be checked even though ETL finishes?
Show solution
It omits the two relations from Purchase, so the target has no foreign keys. ETL’s successful execution confirms that its rules ran; it does not establish that every required source concept was mapped.
Repair the model. Replace the duplicate Customer.email field with active: BOOLEAN, then run the same EVL rules.
Show solution
The corrected model passes all three constraints in this reference rule set:
context Entity {
constraint UniqueEntityName {
check: Entity.all.select(e | e.name = self.name).size() = 1
message: "Duplicate entity: " + self.name
}
}
context Field {
constraint UniqueFieldName {
check: self.eContainer().fields.select(f | f.name = self.name).size() = 1
message: "Duplicate field: " + self.eContainer().name + "." + self.name
}
}
context Relation {
constraint HasTarget {
check: not self.target.isUndefined()
message: "Missing relation target: " + self.name
}
}
More files: shop.flexmi, minijdl.emf
<?nsuri minijdl?>
<application name="Shop">
<entity name="Customer">
<field name="email" type="STRING"/>
<field name="active" type="BOOLEAN"/>
</entity>
<entity name="Product">
<field name="title" type="STRING"/>
<field name="price" type="INTEGER"/>
</entity>
<entity name="Purchase">
<field name="quantity" type="INTEGER"/>
<relation name="customer" target="Shop.Customer"/>
<relation name="product" target="Shop.Product"/>
</entity>
</application>
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
attr String[1] name;
val Entity[*] entities;
}
class Entity {
attr String[1] name;
val Field[*] fields;
val Relation[*] relations;
}
class Field {
attr String[1] name;
attr FieldType type;
}
class Relation {
attr String[1] name;
ref Entity target;
}
All constraints are satisfied.
Extend Java generation. Add an entityName() method to every generated Java class that returns its entity name.
Show solution
The EGX rule still creates one file per entity; the EGL template adds the method:
rule Entity2Java
transform e : Entity {
template: "template.egl"
target: e.name + ".java"
}
public class [%=e.name%] {
public int id;
[% for (f in e.fields) { %] public [% if (f.type.toString() = "STRING") { %]String[% } else if (f.type.toString() = "BOOLEAN") { %]boolean[% } else { %]int[% } %] [%=f.name%];
[% } %][% for (r in e.relations) { %] public [%=r.target.name%] [%=r.name%];
[% } %] public String entityName() { return "[%=e.name%]"; }
}
More files: shop.flexmi, minijdl.emf
<?nsuri minijdl?>
<application name="Shop">
<entity name="Customer">
<field name="email" type="STRING"/>
<field name="active" type="BOOLEAN"/>
</entity>
<entity name="Product">
<field name="title" type="STRING"/>
<field name="price" type="INTEGER"/>
</entity>
<entity name="Purchase">
<field name="quantity" type="INTEGER"/>
<relation name="customer" target="Shop.Customer"/>
<relation name="product" target="Shop.Product"/>
</entity>
</application>
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
attr String[1] name;
val Entity[*] entities;
}
class Entity {
attr String[1] name;
val Field[*] fields;
val Relation[*] relations;
}
class Field {
attr String[1] name;
attr FieldType type;
}
class Relation {
attr String[1] name;
ref Entity target;
}
--- Customer.java ---
public class Customer {
public int id;
public String email;
public boolean active;
public String entityName() { return "Customer"; }
}
--- Product.java ---
public class Product {
public int id;
public String title;
public int price;
public String entityName() { return "Product"; }
}
--- Purchase.java ---
public class Purchase {
public int id;
public int quantity;
public Customer customer;
public Product product;
public String entityName() { return "Purchase"; }
}
Complete the relational mapping. Add an ETL rule that makes a foreign key for each relation and resolves its target table via the transformation trace.
Show solution
Entity2Table adds each _id column and attaches each transformed relation. Relation2ForeignKey maps its target entity to the corresponding table. The target tree now contains customer and product foreign keys under Purchase:
rule Application2Schema
transform a : Source!Application
to s : Target!Schema {
s.name = a.name;
s.tables ::= a.entities;
}
rule Entity2Table
transform e : Source!Entity
to t : Target!Table {
t.name = e.name;
var id = new Target!Column;
id.name = "id";
id.sqlType = "INTEGER";
t.columns ::= e.fields;
t.columns.add(id);
for (r in e.relations) {
var column = new Target!Column;
column.name = r.name + "_id";
column.sqlType = "INTEGER";
t.columns.add(column);
}
t.foreignKeys ::= e.relations;
}
rule Field2Column
transform f : Source!Field
to c : Target!Column {
c.name = f.name;
if (f.type.toString() = "STRING") { c.sqlType = "VARCHAR(255)"; }
else if (f.type.toString() = "BOOLEAN") { c.sqlType = "BOOLEAN"; }
else { c.sqlType = "INTEGER"; }
}
rule Relation2ForeignKey
transform r : Source!Relation
to fk : Target!ForeignKey {
fk.name = r.name;
fk.column = r.name + "_id";
fk.target ::= r.target;
}
@namespace(uri="relational", prefix="relational")
package relational;
class Schema {
attr String name;
val Table[*] tables;
}
class Table {
attr String name;
val Column[*] columns;
val ForeignKey[*] foreignKeys;
}
class Column {
attr String name;
attr String sqlType;
}
class ForeignKey {
attr String name;
attr String column;
ref Table target;
}
More files: shop.flexmi, minijdl.emf
<?nsuri minijdl?>
<application name="Shop">
<entity name="Customer">
<field name="email" type="STRING"/>
<field name="active" type="BOOLEAN"/>
</entity>
<entity name="Product">
<field name="title" type="STRING"/>
<field name="price" type="INTEGER"/>
</entity>
<entity name="Purchase">
<field name="quantity" type="INTEGER"/>
<relation name="customer" target="Shop.Customer"/>
<relation name="product" target="Shop.Product"/>
</entity>
</application>
@namespace(uri="minijdl", prefix="minijdl")
package minijdl;
enum FieldType { STRING; INTEGER; BOOLEAN; }
class Application {
attr String[1] name;
val Entity[*] entities;
}
class Entity {
attr String[1] name;
val Field[*] fields;
val Relation[*] relations;
}
class Field {
attr String[1] name;
attr FieldType type;
}
class Relation {
attr String[1] name;
ref Entity target;
}
Schema name="Shop"
Table name="Customer"
Column name="email" sqlType="VARCHAR(255)"
Column name="active" sqlType="BOOLEAN"
Column name="id" sqlType="INTEGER"
Table name="Product"
Column name="title" sqlType="VARCHAR(255)"
Column name="price" sqlType="INTEGER"
Column name="id" sqlType="INTEGER"
Table name="Purchase"
Column name="quantity" sqlType="INTEGER"
Column name="id" sqlType="INTEGER"
Column name="customer_id" sqlType="INTEGER"
Column name="product_id" sqlType="INTEGER"
ForeignKey name="customer" column="customer_id" target->Customer
ForeignKey name="product" column="product_id" target->Product
Self-check quiz
Field?Relation2ForeignKey find the target table?Further reading
- Dimitris Kolovos, Minimal JHipster JDL Monolith Example (2021), for the motivating whole-application example.
- EVL, EGL, EGX and ETL references for extending the pipeline.