(Abstract) Language
Model
In which language is the model expressed?
In which language is the meta-model expressed?
cf. https://www.omg.org/ocup-2/documents/Meta-ModelingAndtheMOF.pdf
Meta-models are the very first thing you should try to identify whenever approaching a new technology
If you grasp the meta-model, you grasp the essence of the technology
E.g. after you learned the basics of OOP (classes, methods, objects, etc.) you may easily learn any other OOP language
When you model a domain (e.g. with DDD) you are always exploiting some meta-model
Several, slightly similar names, make create confusion
Please read Martin Fowler’s article on Model-Driven Software Architecture to clarify
Despite the name the key ideas can be summarised as follow:
software engineering workflow should start by modelling the domain at hand carefully
the production of a runnable implementation should be automated as much as possible
Assumption: the model is expressed by means of some formal language
Formal language
Formality is a prerequisite for automation
Is UML adequate? Is it the only choice? Any alternative?
Domain-specific Languages (DSL) are programming / description / specification languages targetting one particular class of problems
As opposed to general-purpose languages (GPL) which are targetting as many classes of problems as possible
DSL may act as custom meta-models for a given domain
Examples of DSLs you may already know:
digraph G {
size ="4,4";
main [shape=box];
main -> parse [weight=8];
parse -> execute;
main -> init [style=dotted];
main -> cleanup;
execute -> { make_string; printf}
init -> make_string;
edge [color=red];
main -> printf [style=bold,label="100 times"];
make_string [label="make a string"];
node [shape=box,style=filled,color=".7 .3 1.0"];
execute -> compare;
}
DOT is a language that can describe graphs, for the sake of their visualisation
The most common implementation is the Graphviz toolkit
interface Customer { + CustomerID getID() + String getName() + void **setName**(name: String) + String getEmail() + void **setEmail**(email: String) }
note left: Entity
interface CustomerID { + Object getValue() } note right: Value Object
interface TaxCode { + String getValue() } note left: Value Object
interface VatNumber { + long getValue() } note right: Value Object
VatNumber -d-|> CustomerID TaxCode -d-|> CustomerID
Customer *-r- CustomerID
Scenario: Verify withdraw at the ATM works correctly
Given John has 500$ on his account
When John ask to withdraw 200$
And John inserts the correct PIN
Then 200$ are dispensed by the ATM
And John has 300$ on his account
Gherkin is a language that can describe behavioural tests for software systems
Syntax if very flexible and it seems like natural language
Stakeholders and engineer will agree on a set of behavioural specifications for the system
The most common implementation is Cucumber
DFF : process(RST, CLK) is
begin
if RST = '1' then
Q <= '0';
elsif rising_edge(CLK) then
Q <= D;
end if;
end process DFF;
VHDL is a language that can describe hardware circuits
Seems like an ordinary programming language, but:
Technologies exist to automatically translate VHDL into hardware circuits
… or to simulate the behaviour of the circuit (either in software or in FPGA)
Details here: https://tomassetti.me/financial-accounting-dsl
Goal: use a DSL to describe taxes, pension contributions, and general financial calculations
pension contribution InpsTerziario paid by owner {
considered_salary = (taxable of IRES for employer - amount of IRES for employer - amount of IRAP for employer) by ownership share
rate = brackets [to 46,123] -> 22.74%,
[to 76,872] -> 23.74%,
[above] -> 0%
amount = (rate for considered_salary) with minimum 3,535.61
}
pension contribution InpsGLA paid by employer 2/3 and employee 1/3 {
considered_salary = gross_compensation of employee
rate = brackets [to 100,323] -> 27.72%,
[above] -> 0%
amount = rate for considered_salary
}
To communicate with domain experts in their own language
To let domain experts write the specifications (i.e. the model) of the system they want
To focus on the domain rather than on the implementation
To hide the implementation details from the domain experts
DSLs are not a replacement for GPLs
Yet the difference is fuzzy, so lets try to clarify:
GPL | DSL | |
---|---|---|
Domain | any | clear boundary |
Syntactical constructs | many and composable | few and static |
Expressiveness | Turing-complete | possibly, less than Turing-complete |
Customisability | maximised | minimised / confined / absent |
Defined by | companies or committees | teams of domain expertes |
User base | large, anonymous, widespread | small, accessible, local |
Evolution | slow, well-structured | fast-paced |
Deprecation | very slow | feasible, often abrupt |
Most often, the focus is on the syntax of the DSL
Yet, the semantics of the DSL is equally important
Intuitively, semantics is given to languages by writing the machinery supporting their execution
The role of DSL engineers mostly focuses on steps 1 & 2 (other than defining the syntax)
Two main approaches:
Translation: translates a DSL script into a language for which an execution engine on a given target platform exists
Interpretation: the execution engine is able to parse and execute the DSL script directly
In both cases, there are technical prerequisites:
So far we discussed the so-called external DSLs
As opposed to internal (a.k.a. embedded) DSLs
Creating internal DSL is a recent trend enabled by the wide adoption of flexible GPL
Examples of internal DSL you may already know:
More on this topic in prof. Pianini’s slides
build.gradle.kts
plugins {
`java-library`
}
dependencies {
api("junit:junit:4.13")
implementation("junit:junit:4.13")
testImplementation("junit:junit:4.13")
}
configurations {
implementation {
resolutionStrategy.failOnVersionConflict()
}
}
sourceSets {
main {
java.srcDir("src/core/java")
}
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
tasks {
test {
testLogging.showExceptions = true
useJUnit()
}
}
Internal DSL may ease adoption of the DSL it self
Internal DSL simplify the DSL engineering process
The integration among the GPL and the DSL is tight
Be it internal/external or transpiled/interpreted, the DSL needs an execution engine
This is no different from any other library supporting some given domain
Except that hacks could exploit to ease the adoption of the target DSL syntax
Eclipse’s Xtext widespread tool for MDD
JetBrains’ MPS main competitor of Xtext
Langium clone of Xtext, but based on TypeScript
ANTLR only parser generation for Java, JS, Python, .Net, C++
Language Server Protocol (LSP)
de-facto standard protocol among IDEs
providing various IDE-like capabilities as-a-service
making it easier to support multiple IDEs for the same language
must-have feature for any MDD tool we may consider for our DSL
A framework for MDD and, in particular, external DSL
Xtext provides a language for defining languages…
… which is also a meta-modelling language
Meta-modelling and DSL definition are done simultaneously
It automatically generates the full language infrastructure, including
Exercises and examples about MDD will be based on Xtext
Users may be willing to schedule custom tasks on a machine
Task
bash
, cmd
, powershell
, etc.) of choiceScheduling implied defining when the task should be executed
Notice that tasks may be inter-dependent (e.g. because of before/after relations)
Sheduler
DSL (pt. 1)Sheduler
Shell + Scheduler ¯\(ツ)/¯
We shall use Xtext’s meta-modelling language to define the domain of task scheduling
Simultaneously, we will define the syntax of the DSL
We will then add scoping and validation rules to the DSL, via the Xtext framework
The next step is designing and implementing the execution engine for the DSL
ScheduledExecutorService
s for this purposeFinally, we will create a code generator creating Java code from the DSL
Sheduler
DSL (pt. 2)Code: https://github.com/unibo-spe/sheduler-lang
Clone with Git the exercises
branch
The cloned repository is and Ecplise project
In Eclipse, import the repository root directory as a Gradle project
You may also use IntelliJ, in that case just import the sheduler-lang/
directory as Gradle project
sheduler-lang/
├── build.gradle
├── gradle/
├── gradle.properties
├── gradlew
├── gradlew.bat
├── it.unibo.spe.mdd.sheduler/
│ ├── build.gradle
│ └── src/
│ └── main/
│ └── java/
│ └── it/unibo/spe/mdd/sheduler/
│ └── sheduler
│ ├── GenerateSheduler.mwe2
│ └── Sheduler.xtext
├── it.unibo.spe.mdd.sheduler.ide/
│ └── build.gradle
├── it.unibo.spe.mdd.sheduler.web/
│ └── build.gradle
└── settings.gradle
sheduler
project is where the domain is modelled, and the language is defined
Sheduler.xtext
: this is where modelling and language definition occursGenerateSheduler.mwe2
: this is where the automated generation of scoping, validation, generation, testing facilities is configuredide
project is where the generic IDE support via LSP is defined
sheduler
projectweb
project is where the web-based playground for our language is defined
ide
projectgenerateXtextLanguage
generates the language infrastructure
shadowJar
generates the runnable Jar for the LSP server
jettyRun
starts the Web-playground for the Sheduler language
ordinary Gradle tasks for compilation, testing, etc. are as usual
GenerateSheduler.mwe2
fileThis is automatically generated by Eclipse when setting up an Xtext project
Pretty self-explanatory:
module it.unibo.spe.mdd.sheduler.GenerateSheduler
import org.eclipse.xtext.xtext.generator.*
import org.eclipse.xtext.xtext.generator.model.project.*
var rootPath = ".."
Workflow {
component = XtextGenerator {
configuration = {
project = StandardProjectConfig {
baseName = "it.unibo.spe.mdd.sheduler"
rootPath = rootPath
runtimeTest = { enabled = true }
web = { enabled = true }
mavenLayout = true
}
code = {
encoding = "UTF-8"
lineDelimiter = "\r\n"
fileHeader = "/*\n * generated by Xtext \${version}\n */"
preferXtendStubs = false
}
}
language = StandardLanguage {
name = "it.unibo.spe.mdd.sheduler.Sheduler"
fileExtensions = "shed"
serializer = { generateStub = false }
validator = { generateDeprecationValidation = true }
generator = {
generateXtendStub = false
generateJavaMain = true
}
junitSupport = {
junitVersion = "5"
generateXtendStub = false
generateStub = true
}
}
}
}
grammar it.unibo.spe.mdd.sheduler.Sheduler with org.eclipse.xtext.common.Terminals
generate sheduler "http://www.unibo.it/spe/mdd/sheduler/Sheduler"
TaskPoolSet: pools+=TaskPool+ ;
TaskPool: 'pool' name=ID? '{' tasks+=Task+ '}' ;
Task:
'schedule' ('task' name=ID)? '{'
'command' command=STRING
('entry' 'point' entrypoint=STRING)?
(
'in' relative=RelativeTime |
'at' absolute=AbsoluteTime |
'before' before=[Task] | // square brackets denote references to other model elements
'after' after=[Task]
)
('repeat' 'every' period=RelativeTime)?
'}'
;
AbsoluteTime: date=Date time=ClockTime ;
Date: year=INT '/' month=INT '/' day=INT;
ClockTime: hour=INT ':' minute=INT (':' second=INT (':' millisecond=INT (':' nanosecond=INT)?)?)? ;
RelativeTime: timeSpans += TimeSpan (('and' | ',' | '+') timeSpans += TimeSpan)* ;
TimeSpan: duration=INT unit=(TimeUnit|LongTimeUnit);
enum TimeUnit: NANOSECONDS = 'ns' | MILLISECONDS = 'ms' | SECONDS = 's' | MINUTES = 'm' | HOURS = 'h' | DAYS = 'd' | WEEKS = 'w' | YEARS = 'y' ;
enum LongTimeUnit returns TimeUnit:
NANOSECONDS = 'nanoseconds' |
MILLISECONDS = 'milliseconds' |
SECONDS = 'seconds' |
MINUTES = 'minutes' |
HOURS = 'hours' |
DAYS = 'days' |
WEEKS = 'weeks' |
YEARS = 'years' ;
Classes are generated too!
Run task jettyRun
to start the web playground
Copy-paste the following example program:
pool {
schedule task greetWorldFrequently {
command "echo hello"
entry point "/bin/sh -c"
in 5 minutes
repeat every 1 hours
}
}
What you should see (press Ctrl+Space to see the auto-completion menu)
Validation rules are defined in the ShedulerValidator
class
it.unibo.spe.mdd.sheduler.validation
sheduler-lang/
it.unibo.spe.mdd.sheduler
Stub class is generated by Xtext when running the generateXtextLanguage
task
GenerateSheduler.mwe2
fileContent of the stub and validation rule example:
package it.unibo.spe.mdd.sheduler.validation;
import it.unibo.spe.mdd.sheduler.sheduler.*;
import org.eclipse.xtext.validation.Check;
import org.eclipse.xtext.validation.CheckType;
public class ShedulerValidator extends AbstractShedulerValidator {
@Check(CheckType.FAST)
public void ensureDateIsValid(Date date) {
if (date.getYear() < 0) {
error("Year must be positive", date, ShedulerPackage.Literals.DATE__YEAR, 0);
}
if (date.getMonth() < 1 || date.getMonth() > 12) {
error("Month must be between 1 and 12", date, ShedulerPackage.Literals.DATE__MONTH, 0);
}
if (date.getDay() < 1 || date.getDay() > 31) {
error("Day must be between 1 and 31", date, ShedulerPackage.Literals.DATE__DAY, 0);
}
}
}
Remarks:
@Check
annotation@CheckType
annotation is optional, and defaults to CheckType.NORMAL
FAST
will run whenever a file is modifiedNORMAL
checks will run when saving the file, andEXPENSIVE
checks will run when explicitly validating the file via the menu optionerror(...)
may be replaced by warning(...)
for minor issuesWrite custom validation rules covering the following constraints:
Warning if attempting to represent some RelativeTime
as java.time.Duration
object would result in an overflow
TimeUtils
Warning if attempting to represent some AbsoluteTime
as java.time.LocalDateTime
object would result in an overflow
TimeUtils
Warning if some AbsoluteTime
is in the past or in the present (only future admitted)
TimeUtils
LocalDateTime.now()
LocalDateTime.isBefore()
Error is some ClockTime
is invalid
Error if some TimeSpan
is invalid
Error if any two tasks from the same pool have the same name
Error if any two pools have the same name
No periodicity should be allowed for tasks that are scheduled before/after some other task
Validation rules are defined in the ShedulerScopeProvider
class
it.unibo.spe.mdd.sheduler.scoping
sheduler-lang/
it.unibo.spe.mdd.sheduler
Stub class is generated by Xtext when running the generateXtextLanguage
task
Content of the stub and validation rule example:
public class ShedulerScopeProvider extends AbstractShedulerScopeProvider {
@Override
public IScope getScope(EObject context, EReference reference) {
return super.getScope(context, reference);
}
}
Remarks
IScope
object, for each EObject
containing some EReference
Task
s’ before
and after
propertiesEObject
s, which are the possible values for the EReference
EObject
s to include in the scopeWrite a custom scoping policy for the before
and after
properties of Task
s, such that:
Task
s from the same TaskPool
can be referenced by some Task
Task
s from some TaskPool
cannot be referenced by Task
from other TaskPool
sTask
s (i.e. those without a name) cannot be referenced at allDocumentation here: https://www.eclipse.org/Xtext/documentation/303_runtime_concepts.html#scoping
Two approaches:
Both approaches require the definition of an execution engine
Is there functionality in the JDK which supports the scheduling of tasks in the future?
ScheduledExecutorService
s !Same question for the execution of custom commands via some shell?
ProcessBuilder
s!Design insights:
ShedulerTask
encapsulating:
ProcessBuilder
sShedulerRuntime
ScheduledExecutorService
API…ShedulerTask
s for executionShedulerRuntime
and ShedulerTask
classespublic class ShedulerRuntime {
private final ScheduledExecutorService delegate;
public ShedulerRuntime(ScheduledExecutorService delegate) {
this.delegate = Objects.requireNonNull(delegate);
}
public void schedule(SheduleTask task) {
if (task.isPeriodic()) {
delegate.scheduleWithFixedDelay(
task.asRunnable(),
task.getDelay().toMillis(),
task.getPeriod().toMillis(),
TimeUnit.MILLISECONDS
);
} else {
delegate.schedule(
task.asRunnable(),
task.getDelay().toMillis(),
TimeUnit.MILLISECONDS
);
}
}
}
public class ShedulerTask {
private ShedulerTask(String name, String command, String entrypoint, Duration delay) { /*...*/ }
public String getName() { /*...*/ }
public String getCommand() { /*...*/ }
public String getEntrypoint() { /*...*/ }
public Duration getPeriod() { /*...*/ }
public boolean isPeriodic() { /*...*/ }
public SheduleTask setPeriod(Duration period) { /*...*/ }
public Duration getDelay() { /*...*/ }
public Process executeAsync() throws IOException {
return new ProcessBuilder(entrypoint, command).inheritIO().start();
}
public Runnable asRunnable() {
return () -> {
try {
executeAsync();
} catch (IOException e) {
e.printStackTrace();
}
};
}
public static ShedulerTask in(String name, String command, String entrypoint, Duration delay) { /*...*/ }
public static ShedulerTask in(String command, String entrypoint, Duration delay) { /*...*/ }
public static ShedulerTask at(String name, String command, String entrypoint, LocalDateTime dateTime) { /*...*/ }
public static ShedulerTask at(String command, String entrypoint, LocalDateTime dateTime) { /*...*/ }
}
pool myPool {
schedule task greetFrequently {
command "echo hello"
entry point "/bin/sh -c"
in 5 minutes
repeat every 1 hours
}
schedule task greetOnce {
command "echo hello"
entry point "/bin/bash -c"
at 2030/10/11 12:13
}
}
pool otherPool {
schedule task shutdownAfter1Day {
command "sudo shutdown now"
entry point "/bin/zsh -c"
in 1 days
}
}
public static void main(String[] args) {
ShedulerRuntime runtime = new ShedulerRuntime(Executors.newScheduledThreadPool(1));
pool_myPool(runtime);
pool_otherPool(runtime);
}
private static void pool_myPool(ShedulerRuntime runtime) {
ShedulerTask task0 = ShedulerTask.in("greetFrequently", "echo hello", "/bin/sh -c", Duration.parse("PT5M"));
task0.setPeriodic(Duration.parse("PT1H"));
runtime.schedule(task0);
ShedulerTask task1 = ShedulerTask.at("greetOnce", "echo hello", "/bin/bash -c", LocalDateTime.parse("2030-10-11T12:13"));
runtime.schedule(task1);
}
private static void pool_otherPool(ShedulerRuntime runtime) {
ShedulerTask task0 = ShedulerTask.in("shutdownAfter1Day", "sudo shutdown now", "/bin/zsh -c", Duration.parse("PT24H"));
runtime.schedule(task0);
}
Sheduler
DSLThe code generator should output code having a structure similar to the one shown in the previous slide
The ShedulerRuntime
and ShedulerTask
classes may be generated as they are in the example
The key part is generating the main, where components are assembled together
public class ShedulerGenerator extends AbstractShedulerGenerator {
@Override
public void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) {
File inputFile = new File(resource.getURI().toFileString());
TaskPoolSet taskPools = (TaskPoolSet) resource.getContents().get(0);
String inputFileName = inputFile.getName().split("\\.")[0];
fsa.generateFile("ShedulerRuntime.java", "CODE HERE");
fsa.generateFile("ShedulerTask.java", "CODE HERE");
fsa.generateFile("ShedulerSystem_" + inputFileName + ".java", "CODE HERE");
}
}
Sheduler
DSLNo code generation, just a main
Task
into a ShedulerTask
ShedulerRuntime
The ShedulerRuntime
and ShedulerTask
classes should be part of the runtime
The key part is writing the main
:
public class ShedulerInterpreter {
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("Aborting: no path to .shed file provided!");
return;
}
Injector injector = new ShedulerStandaloneSetup().createInjectorAndDoEMFRegistration();
ShedulerInterpreter interpreter = injector.getInstance(ShedulerInterpreter.class);
interpreter.runFile(args[0]);
}
@Inject private Provider<ResourceSet> resourceSetProvider;
@Inject private IResourceValidator validator;
protected void runFile(String string) {
// Load the resource
ResourceSet set = resourceSetProvider.get();
Resource resource = set.getResource(URI.createFileURI(string), true);
TaskPoolSet taskPools = (TaskPoolSet) resource.getContents().get(0);
ShedulerRuntime runtime = new ShedulerRuntime(Executors.newScheduledThreadPool(1));
for (TaskPool pool : taskPools.getPools()) {
for (Task task : pool.getTasks()) {
runtime.schedule(toShedulerTask(task));
}
}
}
private ShedulerTask toShedulerTask(Task task) {
// TODO
}
}
Add support for task dependencies (before/after) to the execution engine
Update the code generator accordingly
Update the interpreter accordingly