Two metamodels and one mapping
The component model describes ports and wiring. A graph model needs only nodes and directed edges. Model-to-model transformation translates between these languages: the source model conforms to the component metamodel, and the target model conforms to a separate graph metamodel. The transformation must decide what to preserve, what to omit and how to reconnect references.
Here, each Architecture becomes a Graph, each Component becomes a Node, and each Connector becomes an Edge. Port names disappear in the simplest version. This is deliberate: a graph algorithm can work at component level without knowing the component language’s port details. ETL, the Epsilon Transformation Language, defines the mapping in rules. Its rule bodies use EOL expressions from Module 4.
Write ETL rules
The first program has three rules. Architecture2Graph creates the root and populates its contained nodes and edges. Component2Node copies a component name. Connector2Edge links the graph nodes corresponding to the components at its two endpoint ports.
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
g.nodes ::= a.components;
g.edges ::= a.connectors;
}
rule Component2Node
transform c : Source!Component
to n : Target!Node {
n.name = c.name;
}
rule Connector2Edge
transform c : Source!Connector
to e : Target!Edge {
e.source ::= c.source.eContainer();
e.target ::= c.target.eContainer();
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate"
Node name="AndGate"
Node name="Siren"
Edge source->OrGate target->AndGate
Edge source->AndGate target->Siren
Read the captured target tree. It has a graph named Alarm, three nodes and two edges. The target metamodel is visible under the ETL program. Source!Component and Target!Node specify types in different models; they avoid mistaking a source component for a target node. g.nodes ::= a.components uses ETL’s special assignment: assign each source component’s transformed counterpart, not the source component itself.
A transformation is not just an export. It creates another model with its own types and relationships, ready for another model-management task. The local runner prints the target model’s containment tree so you can inspect it; it does not persist the target as an XMI file for this lesson.
Resolve transformed counterparts
ETL keeps a transformation trace connecting source elements to the target elements produced for them. equivalent() asks for a source element’s transformed counterpart. The next program performs the same mapping as the first, but calls equivalent() explicitly when it fills the target graph and links edge endpoints.
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
for (c in a.components) { g.nodes.add(c.equivalent()); }
for (wire in a.connectors) { g.edges.add(wire.equivalent()); }
}
rule Component2Node
transform c : Source!Component
to n : Target!Node { n.name = c.name; }
rule Connector2Edge
transform c : Source!Connector
to e : Target!Edge {
e.source = c.source.eContainer().equivalent();
e.target = c.target.eContainer().equivalent();
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate"
Node name="AndGate"
Node name="Siren"
Edge source->OrGate target->AndGate
Edge source->AndGate target->Siren
Compare the two outputs: they are the same graph. ::= is shorthand for the common case in which a target feature receives an equivalent target element. An explicit equivalent() is useful when you want to calculate, select or combine counterparts in a larger expression. Neither form is a lookup by matching names: the trace records which rule created which result.
Guards and lazy rules
A guard restricts which source elements a rule transforms. In the next source model, the second connector joins a digital output to an analog input. The guarded connector rule creates an edge only when the two port types match. All three components still become nodes, but only the compatible connection becomes an edge.
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
g.nodes ::= a.components;
g.edges ::= a.connectors;
}
rule Component2Node
transform c : Source!Component
to n : Target!Node {
n.name = c.name;
}
rule Connector2Edge
transform c : Source!Connector
to e : Target!Edge {
guard: c.source.type = c.target.type
e.source ::= c.source.eContainer();
e.target ::= c.target.eContainer();
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="ANALOG"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate"
Node name="AndGate"
Node name="Siren"
Edge source->OrGate target->AndGate
This is a policy choice, not a replacement for validation. Silently dropping an invalid connector may be dangerous for a downstream task. A project can first run EVL to reject the model, or deliberately transform only valid connections while reporting omissions. The example isolates the guard’s effect so you can see it.
A lazy rule runs only when an equivalent is requested. The next example maps ports, rather than components, to graph nodes. Architecture2Graph requests Port2Node for each port. Because Port is abstract and the instances are InPort and OutPort, the rule uses @greedy to match subclasses as well as the exact type.
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
for (p in Port.all) { g.nodes.add(p.equivalent("Port2Node")); }
}
@lazy
@greedy
rule Port2Node
transform p : Source!Port
to n : Target!Node {
n.name = p.eContainer().name + "." + p.name;
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate.door"
Node name="OrGate.window"
Node name="OrGate.open"
Node name="AndGate.open"
Node name="AndGate.armed"
Node name="AndGate.sound"
Node name="Siren.sound"
The result has seven nodes labelled with both the owning component and port name. Without @greedy, this rule would not apply to the concrete port subclasses in Epsilon 2.8.0; the requested counterparts would be missing. @lazy avoids creating port nodes unless a rule asks for them. Use it for results that are conditional or expensive, not to conceal rules whose products should always exist.
Primary rules and trace
Sometimes more than one rule transforms the same source type. In the next example, Component2Node makes the ordinary node and Component2Audit makes a second, audit node. @primary puts the ordinary node first when equivalent() chooses one counterpart.
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
for (c in a.components) { g.nodes.add(c.equivalent()); }
}
@primary
rule Component2Node
transform c : Source!Component
to n : Target!Node { n.name = c.name; }
rule Component2Audit
transform c : Source!Component
to n : Target!Node { n.name = "audit:" + c.name; }
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate"
Node name="AndGate"
Node name="Siren"
Node name="audit:OrGate"
Node name="audit:AndGate"
Node name="audit:Siren"
The graph contains the three ordinary nodes. The audit nodes are also produced by their non-lazy rule; the local target printer shows them as separate roots because no graph contains them. Primary does not suppress other rules. It controls which counterpart comes first for a single-element equivalent() lookup. If a target model should contain only graph-owned nodes, remove the audit rule or give its results a deliberate home. The ETL reference describes rule scheduling, equivalent() and equivalents().
Check the target model
A transformation can run successfully yet lose information you needed. The basic graph drops port names and signal types. The guarded graph drops an incompatible connection. The primary-rule graph has extra uncontained audit nodes. Read the target metamodel and the printed target tree together to see exactly what exists after transformation.
For a real mapping, write down the intended invariants: the number of nodes expected, which connectors become edges, whether names remain unique, and whether every edge endpoint belongs to the target graph. Validate the source before transforming when the mapping assumes well-formed wiring, then inspect or validate the target too. Module 10 applies this pattern to a second domain.
Exercises
Map the types. List the source type that becomes each of Graph, Node and Edge. Which source information does the basic target omit?
Show solution
Architecture becomes Graph, Component becomes Node, and Connector becomes Edge. The basic target omits ports, their names and signal types; it keeps only component names and component-level connections.
Read ::=. Why is g.nodes ::= a.components meaningful while assigning a.components directly to target Node containment would be wrong?
Show solution
The right-hand side contains source Component elements. ETL’s ::= resolves their transformed Node counterparts and assigns those to g.nodes. Direct assignment would mix elements from incompatible metamodels.
Interpret primary. In the primary example, why are audit nodes printed even though the graph’s nodes contain only ordinary nodes?
Show solution
Component2Audit is non-lazy, so it still runs for every component. @primary makes ordinary nodes the first counterpart selected by equivalent(); it does not disable the audit rule. The audit nodes remain separate target roots because nothing contains them.
Keep an edge label. Extend the target Edge with a label attribute and set it to <source port> to <target port> in Connector2Edge. Check that two labelled edges are present.
Show solution
The target metamodel already has the label feature in this module’s examples. The rule assigns it while retaining both endpoint mappings:
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
g.nodes ::= a.components;
g.edges ::= a.connectors;
}
rule Component2Node
transform c : Source!Component
to n : Target!Node {
n.name = c.name;
}
rule Connector2Edge
transform c : Source!Connector
to e : Target!Edge {
e.label = c.source.name + " to " + c.target.name;
e.source ::= c.source.eContainer();
e.target ::= c.target.eContainer();
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate"
Node name="AndGate"
Node name="Siren"
Edge label="open to open" source->OrGate target->AndGate
Edge label="sound to sound" source->AndGate target->Siren
Test a guarded model. Run the guarded transformation on the correctly typed alarm. How many edges should the result contain, and why?
Show solution
Both connectors pass the type guard, so both become edges:
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
g.nodes ::= a.components;
g.edges ::= a.connectors;
}
rule Component2Node
transform c : Source!Component
to n : Target!Node {
n.name = c.name;
}
rule Connector2Edge
transform c : Source!Connector
to e : Target!Edge {
guard: c.source.type = c.target.type
e.source ::= c.source.eContainer();
e.target ::= c.target.eContainer();
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate"
Node name="AndGate"
Node name="Siren"
Edge source->OrGate target->AndGate
Edge source->AndGate target->Siren
Request only output ports. Adapt the lazy example so the graph receives nodes only for OutPort instances. Use their full Component.Port labels. Do you still need @greedy?
Show solution
The rule now matches the exact concrete OutPort type, so @greedy is unnecessary. The graph contains the two output ports:
rule Architecture2Graph
transform a : Source!Architecture
to g : Target!Graph {
g.name = a.name;
for (p in OutPort.all) { g.nodes.add(p.equivalent("Port2Node")); }
}
@lazy
rule Port2Node
transform p : Source!OutPort
to n : Target!Node {
n.name = p.eContainer().name + "." + p.name;
}
@namespace(uri="graph", prefix="graph")
package graph;
class Graph {
attr String name;
val Node[*] nodes;
val Edge[*] edges;
}
class Node { attr String name; }
class Edge {
attr String label;
ref Node source;
ref Node target;
}
More files: alarm.flexmi, components.emf
<?nsuri components?>
<architecture name="Alarm">
<component name="OrGate">
<inPort name="door" type="DIGITAL"/>
<inPort name="window" type="DIGITAL"/>
<outPort name="open" type="DIGITAL"/>
</component>
<component name="AndGate">
<inPort name="open" type="DIGITAL"/>
<inPort name="armed" type="DIGITAL"/>
<outPort name="sound" type="DIGITAL"/>
</component>
<component name="Siren">
<inPort name="sound" type="DIGITAL"/>
</component>
<connector source="Alarm.OrGate.open" target="Alarm.AndGate.open"/>
<connector source="Alarm.AndGate.sound" target="Alarm.Siren.sound"/>
</architecture>
@namespace(uri="components", prefix="components")
package components;
enum PortType {
DIGITAL;
ANALOG;
}
class Architecture {
attr String[1] name;
val Component[*]#architecture components;
val Connector[*] connectors;
}
class Component {
attr String[1] name;
ref Architecture#components architecture;
val Port[*] ports;
}
abstract class Port {
attr String[1] name;
attr PortType type;
}
class InPort extends Port {}
class OutPort extends Port {}
class Connector {
ref OutPort source;
ref InPort target;
}
Graph name="Alarm"
Node name="OrGate.open"
Node name="AndGate.sound"
Self-check quiz
Source!Component identify?::= do here?Connector2Edge control?Port2Node rule use @greedy?@primary guarantee in the example?Further reading
- The ETL reference documents rules, guards, traces, lazy and primary behaviour.
- The EOL reference explains expressions used inside rule bodies.