Ran Wei/MDE 系列
English
基于 Eclipse Epsilon 的模型驱动工程 — Ran Wei

从 Playground 到真实项目

安装 Eclipse Epsilon,通过 Ant、Maven 和 Java 运行程序,并处理非 EMF 模型。

4 小时4 个时段8 个可运行示例Epsilon Playground

完成后你能够

  • 选择本地 Epsilon 环境并导出示例。
  • 说明 Ant、Maven 与 Java 的运行路径。
  • 辨认非 EMF 模型驱动与后续可探索工具。

开始之前

建议先学习第 1 至 8 模块。除特别说明外,示例均可在 Playground 中运行。

下载本模块全部示例(zip)

目录

学习计划

4 小时

时间为估计值,包含运行示例的时间。进度保存在当前浏览器,中英文版本共享。

时段 160 分钟
走出浏览器:设置 Epsilon
时段 260 分钟
用 Ant、Maven 和 Java 运行
时段 360 分钟
其他模型格式与项目工作流
时段 460 分钟
练习
1

走出浏览器

Playground 让前八个模块无需安装即可运行。真实项目通常还需要纳入版本管理的文件、可重复的构建、本地数据、磁盘上的生成文件,以及与其他工具的集成。离开浏览器时,仍保留元模型、模型和程序这三项输入。先导出一个能运行的示例,再改变执行环境,并将本地结果与本页记录的结果比较。

本模块的下载包包含示例文件。Playground 自带的 Download 按钮还可把当前示例打包成 Eclipse Ant、Maven、Gradle 或 Java 项目,并附带 README;参见 Playground 下载指南。如果在本教程的本地仓库中工作,Maven 运行器源码展示了针对同一组文件运行 EOL、EVL、EGL、EGX 和 ETL 的 Java 集成。

2

设置 Eclipse Epsilon

Epsilon 下载页介绍了两种方式:在 Eclipse Installer 中选择 Epsilon,或在现有 Eclipse 中打开 Help → Install New Software,使用稳定版更新站点 https://download.eclipse.org/epsilon/updates/2.8/。当前稳定的 Epsilon 2.8 系列与本教程记录的运行版本一致。新建项目,解压下载包,查看 components.emf、alarm.flexmi 和 inspect.eol。配置 EOL 运行时,将模型命名为 M,以 Flexmi 文件为模型输入、components.emf 为元模型。如果程序通过名称或限定类型访问模型,模型名称尤为重要。

先在 Playground 中运行查询,再在本地运行。两处都应列出警报模型及其三个组件。如果本地加载失败,先检查元模型命名空间、文件路径及连线引用的完整路径,再改动 EOL 程序。

示例检查导出的模型在 Playground 中打开 ↗
inspect.eol
("Architecture: " + Architecture.all.first().name).println();
for (c in Component.all) {
  (c.name + ": " + c.ports.size() + " ports").println();
}
其他文件: alarm.flexmi, components.emf
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Architecture: Alarm
OrGate: 3 ports
AndGate: 3 ports
Siren: 1 ports

示例文件刻意保持简短。学习 Eclipse 运行配置时,可以把输出当作已知参照。Epsilon 入门指南和 Playground 导出项目中的 README 提供适合所用安装版本的界面步骤。

3

用 Ant 与 Maven 运行

Ant 工作流指定要加载的模型及要运行的程序。Epsilon 提供 EOL 等语言的 Ant 任务。典型的 EMF 任务加载 Ecore 元模型和模型,然后 EOL 任务引用该模型:

<epsilon.emf.loadModel name="M" modelFile="alarm.xmi" metamodelFile="components.ecore"/>
<epsilon.eol src="inspect.eol"><model ref="M"/></epsilon.eol>

这段代码假定已把 Emfatic 和 Flexmi 输入导出为 Ecore 和 XMI。若继续使用教程的 .emf 与 .flexmi 文件,请从 Playground 的 Ant (Eclipse) 下载项目开始;它包含必要配置。也可参照完整的 Epsilon 命令行 Ant 示例。只有上面两行任务片段,既不会安装 Ant 任务,也不会转换文件格式。

对于 Maven,在 Playground 的下载菜单选择 Maven,进入解压后的项目执行 mvn clean package。导出的 pom.xml 提供依赖并调用 Epsilon 任务。在真实项目中应固定 Epsilon 版本;下载页列出了 2.8.0 版 Maven 坐标。把验证放在生成之前,避免无效模型悄悄产出交付文件。下面的 EVL 关卡检查连线的信号类型:

示例在本地运行验证关卡在 Playground 中打开 ↗
rules.evl
context Connector {
  constraint Compatible {
    check: self.source.type = self.target.type
    message: "Incompatible signal types"
  }
}
其他文件: alarm.flexmi, components.emf
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
All constraints are satisfied.

本地运行时,除了退出状态,还要查看验证结果。对于验证发现的问题,构建必须有明确策略:报告问题、使构建失败,或允许有记录的例外。

4

从 Java 运行

在 Playground 的下载菜单选择 Java (Maven),可得到小型 Java 项目和当前示例的 Epsilon API 配置。Java 中的步骤是:创建如 EolModule 的模块、解析程序、通过 EMC 驱动加载模型、把模型加入模块仓库、执行模块,最后释放资源。教程运行器的 Run.java提供了更完整的实现。它的 pom.xml 固定 Epsilon 2.8.0;网站构建用它捕获每个示例的输出。

下面的 EGL 模板可用于检查集成结果:Java 可捕获其文本,构建也可将其写入文件。应将数值与模型比较,不要仅凭模板完成运行就假定产物正确。

示例生成本地报告在 Playground 中打开 ↗
generate.egl
Architecture: [%=Architecture.all.first().name%]
Components: [%=Component.all.size()%]
Connectors: [%=Connector.all.size()%]
其他文件: alarm.flexmi, components.emf
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Architecture: Alarm
Components: 3
Connectors: 2

要生成多个文件,可以让 EGX 对每个匹配元素调用一次 EGL 模板。本地运行时需要选择可写的输出目录;Playground 则在输出面板展示生成文件。

示例为每个组件生成摘要在 Playground 中打开 ↗
generate.egx
rule Component2Summary
  transform c : Component {
  template: "template.egl"
  target: c.name + ".txt"
}
template.egl
[% var noun = "ports"; if (c.ports.size() = 1) { noun = "port"; } %][%=c.name + ": " + c.ports.size() + " " + noun%]
其他文件: alarm.flexmi, components.emf
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
--- AndGate.txt ---
AndGate: 3 ports
--- OrGate.txt ---
OrGate: 3 ports
--- Siren.txt ---
Siren: 1 port
5

其他模型格式与工具

Epsilon 的 EMC 层让各语言连接不同的模型技术。EMF 是其中一种驱动,并不是模型的唯一形式。EMC 指南列出 CSV、XML、JSON、YAML 等驱动。例如,CSV 驱动指南在 EOL 中使用 Row.all,并在 Ant 中使用 epsilon.csv.loadModel。CSV 行也是可查询的元素,但不遵循这些 Playground 示例所用的组件元模型。移植规则之前,先用小样本确认驱动的类型名、特征名和持久化行为。

项目需求变化后,其他 Epsilon 工具也会派上用场:ECL比较模型,EML合并模型,Flock迁移旧模型,EUnit测试 Epsilon 程序,Picto在 Eclipse 中展示模型视图。这些是后续学习的起点,不是完成结课项目的前提。

6

规划真实项目

确定哪个模型是权威来源、如何验证、哪些产物会重新生成,以及哪些文件纳入版本管理。在持续集成中保留一个小型端到端示例。如果转换结果还要交给生成器,也应验证目标模型。下面的 ETL 示例重用组件到图的映射:本地工作流可先对源模型运行 EVL,再运行 ETL,检查图模型,最后生成图报告。

示例导出前进行转换在 Playground 中打开 ↗
transform.etl
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();
}
graph.emf
@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;
}
其他文件: alarm.flexmi, components.emf
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Graph name="Alarm"
  Node name="OrGate"
  Node name="AndGate"
  Node name="Siren"
  Edge source->OrGate target->AndGate
  Edge source->AndGate target->Siren

记录工具版本、输入文件和生成产物。决定如何处理人工修改;第 6 模块介绍了受保护区域,但真实项目应先在可丢弃副本上测试重新生成。一个实用的本地里程碑是:通过一条命令或一次 Eclipse 启动,得到与本页相同的组件数量、验证结果和报告文本。

7

练习

练习 1★★★概念5 分钟

选择下载方式。 Eclipse Ant 工作流应选择哪个 Playground 下载选项?由 Maven 构建、通过 API 调用的 Java 程序又该选哪个?

查看解答

Eclipse 工作流选择 Ant (Eclipse),带有 Maven 依赖的 Java API 项目选择 Java (Maven)。阅读各自导出项目的 README 并检查路径。

练习 2★★★概念5 分钟

安排工作流。 按合理顺序排列模型加载、验证、转换和生成。目标模型应在何处检查?

查看解答

加载源模型,验证源模型,执行转换,检查或验证目标模型,最后生成文本。工作流结束后释放模型。如果源模型验证失败,可在转换前停止。

练习 3★★★概念5 分钟

选择驱动。 团队收到 CSV 库存和 XML 配置。Epsilon 哪一层让 EOL 访问它们?重用组件规则前应检查什么?

查看解答

EMC 提供 CSV 和 XML 模型驱动。检查驱动中的元素及特征名称,以及数据的加载和保存方式;针对 Component 写的规则不会自动适用于 CSV 的 Row 或 XML 节点。

练习 4★★★编程10 分钟

检查导出文件。 编写 EOL 查询,将每个输出端口打印成 组件.端口,以便比较本地运行与 Playground 的结果。

查看解答

查询选择具体的输出端口,并打印其所属组件名称:

参考解答列出源端口名称在 Playground 中打开 ↗
inspect.eol
for (c in Component.all) {
  for (p in c.ports.select(p | p.isTypeOf(OutPort))) {
    (c.name + "." + p.name).println();
  }
}
其他文件: alarm.flexmi, components.emf
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
OrGate.open
AndGate.sound
练习 5★★★编程10 分钟

加入本地关卡。 在生成之前,要求组件名称各不相同。在警报模型上测试约束。

查看解答

警报模型中各组件名称不同,因此这次 EVL 运行通过:

参考解答要求组件名称各不相同在 Playground 中打开 ↗
rules.evl
context Component {
  constraint UniqueName {
    check: Component.all.select(c | c.name = self.name).size() = 1
    message: "Duplicate component name: " + self.name
  }
}
其他文件: alarm.flexmi, components.emf
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
All constraints are satisfied.
练习 6★★★编程10 分钟

生成交接清单。 生成简短的文本清单,包含模型名称、组件名称和运行验证的提醒。

查看解答

EGL 模板从模型读取名称,并打印验证步骤:

参考解答生成交接清单在 Playground 中打开 ↗
generate.egl
Model: [%=Architecture.all.first().name%]
[% for (c in Component.all) { %]Component: [%=c.name%]
[% } %]Validation: run rules.evl before deployment
其他文件: alarm.flexmi, components.emf
alarm.flexmi
<?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>
components.emf
@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;
}
以下输出由构建本页时的 Epsilon 2.8.0 实际运行得到。Playground 中的提示措辞可能有所不同,并且不显示关于模型的警告。
输出
Model: Alarm
Component: OrGate
Component: AndGate
Component: Siren
Validation: run rules.evl before deployment
8

自测

1
将示例移出 Playground 后应该比较什么?
2
哪种下载包包含调用 Epsilon API 的 Java 类?
3
Ant 中的 model ref="M" 指向什么?
4
为什么要固定 Epsilon 依赖版本?
5
哪一层把 Epsilon 语言连接到 CSV 和 XML 模型?
6
Flock 主要解决什么问题?
7
生成交付文件前应做什么?
9

延伸阅读