从模型到文本
一个模型可以成为多种输出的共同来源。报警器的组件名称和连接关系可以生成零件清单、HTML 报告、Graphviz 图描述或 Java 源代码。模型到文本转换(model-to-text transformation)从模型派生这些产物;模型变化后可以重新生成。但生成结果不会自动正确:生成器必须转义数据、使用合法标识符,并且只在模型满足必要规则时运行。
Epsilon 使用 EGL(Epsilon Generation Language)编写模板,用 EGX 协调模板执行与目标文件。两者都沿用第 4 模块的 EOL 导航。本模块示例下面的实际输出是生成的文本;对于 EGX,本地运行器还标出每个生成文件的名称。先从小型报警器入手,再尝试修改模型并重新运行模板。
编写 EGL 模板
EGL 模板混合文本和动态片段。[% ... %] 之外的文本原样复制。[% ... %] 执行 EOL 语句;[%= 表达式 %] 插入表达式的值。第一个示例循环访问每个组件,为它写一行端口数量。
Architecture: [%=Architecture.all.first().name%]
[% for (c in Component.all) { %]
- [%=c.name%]: ports=[%=c.ports.size()%]
[% } %]
其他文件: 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;
}
Architecture: Alarm
- OrGate: ports=3
- AndGate: ports=3
- Siren: ports=1
模板有固定标题和动态列表。Architecture.all.first().name 提供架构名称。如果在 Flexmi 模型中加入组件,无需改动模板就会多出一行。生成的文件是派生产物:应以模型和模板为源,修改后重新生成。
模板的静态文字决定输出的形状;动态 EOL 表达式提供从模型取得的值。两者共同组成可读的生成文档。
生成多种格式
EGL 不局限于一种目标语言。下面的模板把 HTML 标签作为静态文字,并把组件名称与数量插入表格单元格。
<h2>[%=Architecture.all.first().name%]</h2>
<table>
<tr><th>Component</th><th>Inputs</th><th>Outputs</th></tr>
[% for (c in Component.all) { %]
<tr><td>[%=c.name%]</td><td>[%=c.ports.select(p | p.isTypeOf(InPort)).size()%]</td><td>[%=c.ports.select(p | p.isTypeOf(OutPort)).size()%]</td></tr>
[% } %]
</table>
其他文件: 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;
}
<h2>Alarm</h2>
<table>
<tr><th>Component</th><th>Inputs</th><th>Outputs</th></tr>
<tr><td>OrGate</td><td>2</td><td>1</td></tr>
<tr><td>AndGate</td><td>2</td><td>1</td></tr>
<tr><td>Siren</td><td>1</td><td>0</td></tr>
</table>
Playground 可能渲染 HTML;下方的实际输出显示精确的标记文本。真实的生成器在把名称插入 HTML 前,应先做转义。教学模型只使用简单名称,因此这里聚焦 EGL 结构,而非完整的 HTML 转义库。
下一个模板生成 Graphviz DOT,一种图的文本描述。每个组件成为节点;每个连接器成为其端点端口所属组件之间的箭头。
digraph Alarm {
[% for (c in Component.all) { %]
"[%=c.name%]";
[% } %]
[% for (wire in Connector.all) { %]
"[%=wire.source.eContainer().name%]" -> "[%=wire.target.eContainer().name%]";
[% } %]
}
其他文件: 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;
}
digraph Alarm {
"OrGate";
"AndGate";
"Siren";
"OrGate" -> "AndGate";
"AndGate" -> "Siren";
}
按本系列使用的模型驱动工程术语,DOT 在这里是另一种具体输出,并非新的领域模型。它省略了端口细节:即使两个报警器模型的端口名称不同,也可能生成相同的组件级图。要有意识地决定目标产物需要哪些信息。生成的图便于审查,而文本预览也能让边的具体数据清楚可见。
用 EGX 为每个元素生成文件
单个 EGL 模板自然地产生一段文本。EGX 为选定的模型元素运行模板,并决定每个结果保存到哪里。下面的规则对每个 Component 运行一次,把该组件作为 c 传给 template.egl,并用 c.name 命名输出文件。
rule Component2Java
transform c : Component {
template: "template.egl"
target: c.name + ".java"
}
public final class [%=c.name%] {
public static final int INPUTS = [%=c.ports.select(p | p.isTypeOf(InPort)).size()%];
public static final int OUTPUTS = [%=c.ports.select(p | p.isTypeOf(OutPort)).size()%];
}
其他文件: 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;
}
--- AndGate.java ---
public final class AndGate {
public static final int INPUTS = 2;
public static final int OUTPUTS = 1;
}
--- OrGate.java ---
public final class OrGate {
public static final int INPUTS = 2;
public static final int OUTPUTS = 1;
}
--- Siren.java ---
public final class Siren {
public static final int INPUTS = 1;
public static final int OUTPUTS = 0;
}
实际输出有三个文件标题和三个 Java 类。template 指明 EGL 模板;target 计算文件名。模板用 EOL 的 select 统计输入和输出端口。用于生产环境时,应先验证每个组件名称都是安全且唯一的 Java 标识符,并转义插入源代码的文本。这里固定的示例名称符合这些要求。
EGX 规则还支持守卫条件、参数及其他工作流程控制。EGX 参考文档有详细说明。把“生成哪些文件”的决定放在 EGX,把“每个文件写什么”放在 EGL,各自更易修改。
受保护区域
重新生成通常会覆盖生成文件中的手工修改。EGL 可以标记受保护区域(protected region),让文件生成流程在已有目标文件中保留指定片段。示例用 out.preserve 生成名为 manual-note 的区域。
public final class AlarmNotes {
[%=out.preserve("/*", "*/", "manual-note", true, " // Add project-specific notes here")%]
}
其他文件: 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;
}
public final class AlarmNotes {
/* protected region manual-note on begin */
// Add project-specific notes here
/* protected region manual-note end */
}
实际输出展示了标记及初始内容,但没有展示与现有文件合并:本模块的本地运行器处理模板时,没有旧目标文件可供合并;Playground 也不是 Eclipse 的文件生成工作流程。要在本地项目中测试保留行为,先生成文件,只编辑标记区域,再启用合并并向同一目标重新生成。应明确区分生成内容和手写内容;受保护区域不能代替对模型或模板改动的审查。EGL 合并文档解释了标记与合并行为。
审查生成结果
使用生成文本前,应问清它依赖什么。Java 生成器假定名称可以作类标识符;HTML 生成器假定名称能安全插入标记;Graphviz 生成器忽略端口和信号类型。模板本身不验证连接器是否合理。如果输出依赖这些假设,应先运行第 5 模块的 EVL 规则。
实用的生成流程分三步:验证模型、以确定方式生成,然后检查或测试产物。这里的输入输出都足够小,可以逐项对照。真实项目还应把生成纳入构建,并在目标环境中测试代码或文档。第 9 模块会介绍怎样在 Playground 之外运行 Epsilon。
练习
静态还是动态? 在 HTML 表格模板中,将 <tr><td>、[%=c.name%] 和 [% for (c in Component.all) { %] 分别归为静态文字、值插入或可执行模板代码。
查看解答
<tr><td> 是原样复制的静态文字。[%=c.name%] 插入当前组件的值。for 代码块是可执行 EOL 代码,控制后续文字输出多少次。
丢失了什么? Graphviz 示例为每个组件创建节点、为每个连接器创建边。举出源模型中未被生成图记录的两个事实。
查看解答
图省略了具体端口名称及其 DIGITAL/ANALOG 类型,也没有指出每个连接器究竟连接哪些端口。生成器有意选择组件级视图。
谁负责什么决定? Java 示例中,哪个文件决定输出文件名,哪个文件决定类的内容?使用这些类之前应检查什么?
查看解答
EGX 规则计算 c.name + ".java";template.egl 写类的内容。应验证类名合法且唯一,并检查生成代码所依赖的模型规则。在真实工作流程中还应编译或测试生成的 Java。
简洁 HTML。 编写 EGL 模板,生成一个无序列表,每个组件占一项,并显示端口数量。
查看解答
循环包住静态的 <li> 行,表达式插入其中的取值:
<ul>
[% for (c in Component.all) { %]
<li>[%=c.name%] (ports: [%=c.ports.size()%])</li>
[% } %]
</ul>
其他文件: 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;
}
<ul>
<li>OrGate (ports: 3)</li>
<li>AndGate (ports: 3)</li>
<li>Siren (ports: 1)</li>
</ul>
每个组件一个摘要。 编写 EGX 规则,为每个组件调用一次 template.egl,写入 <名称>.txt。每个文件应报告该组件的端口数量。
查看解答
规则选择目标文件,模板生成内容。本地输出展示三个独立文件:
rule Component2Summary
transform c : Component {
template: "template.egl"
target: c.name + ".txt"
}
[%=c.name%]: ports=[%=c.ports.size()%].
其他文件: 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;
}
--- AndGate.txt ---
AndGate: ports=3.
--- OrGate.txt ---
OrGate: ports=3.
--- Siren.txt ---
Siren: ports=1.
给边加标签。 修改 DOT 模板,使每个连接器箭头都标上其源端口与目标端口的名称。
查看解答
新增的 label 使用 wire.source.name 与 wire.target.name:
digraph Alarm {
[% for (wire in Connector.all) { %]
"[%=wire.source.eContainer().name%]" -> "[%=wire.target.eContainer().name%]" [label="[%=wire.source.name%] to [%=wire.target.name%]"];
[% } %]
}
其他文件: 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;
}
digraph Alarm {
"OrGate" -> "AndGate" [label="open to open"];
"AndGate" -> "Siren" [label="sound to sound"];
}
自测
[%=c.name%] 做什么?延伸阅读
- EGL 参考文档介绍模板与受保护区域。
- EGX 参考文档介绍基于规则的模板调度。
- Epsilon 代码生成教程给出规模更大的示例。