XDef
- 文章作者: flytreeleft - flytreeleft@crazydan.org
- 文章链接: https://nop.crazydan.io/docs/manual/xlang/xdef
- 版权声明: 本文章采用许可协议《署名 4.0 国际 (CC BY 4.0)》,转载或商用请注明文章来源及作者信息。
在 Nop 中,所有的 DSL 均需要由与其对应的 XDef 模型来定义和约束其结构,该 XDef 模型即为 DSL 的元模型。
对 XDef 结构的定义(即,元模型的元模型)见 /nop/schema/xdef.xdef。
定义
<meta:unknown-tag
xmlns:x="/nop/schema/xdsl.xdef" xmlns:xdef="xdef"
xmlns:meta="/nop/schema/xdef.xdef" x:schema="/nop/schema/xdef.xdef"
meta:ref="XDefNode"
xdef:base="v-path"
xdef:default-extends="v-path"
xdef:version="string"
xdef:bean-package="package-name"
xdef:transform="v-path"
xdef:transformer-class="class-name-set"
xdef:parse-for-html="boolean"
xdef:parse-keep-comment="boolean"
xdef:parser-class="class-name"
xdef:check-ns="word-set" xdef:prop-ns="word-set"
xdef:model-name-prop="string"
xdef:model-version-prop="string"
>
<xdef:pre-parse meta:value="xpl" />
<xdef:post-parse meta:value="xpl" />
<meta:define meta:name="XDefNode"
xdef:name="var-name" xdef:ref="xdef-ref"
xdef:value="def-type" xdef:unique-attr="xml-name"
xdef:body-type="enum:io.nop.xlang.xdef.XDefBodyType"
xdef:key-attr="xml-name"
xdef:order-attr="xml-name"
xdef:mandatory="boolean" xdef:internal="boolean" xdef:deprecated="boolean"
xdef:allow-multiple="boolean"
xdef:support-extends="boolean"
xdef:default-override="enum:io.nop.xlang.xdef.XDefOverride"
xdef:unknown-attr="def-type"
xdef:bean-class="class-name"
xdef:bean-prop="prop-name"
xdef:bean-body-prop="prop-name"
xdef:bean-body-type="generic-type"
xdef:bean-ref-prop="prop-name"
xdef:bean-unknown-attrs-prop="prop-name"
xdef:bean-unknown-children-prop="prop-name"
xdef:bean-child-name="var-name"
xdef:bean-comment-prop="prop-name"
xdef:bean-sub-type-prop="prop-name"
xdef:bean-tag-prop="prop-name"
xdef:bean-extends-type="generic-type"
xdef:bean-implements-types="generic-type-list"
>
<xdef:unknown-tag meta:ref="XDefNode" />
<xdef:define meta:ref="XDefNode" meta:unique-attr="xdef:name"
xdef:name="!var-name"
/>
</meta:define>
</meta:unknown-tag>
xdef.xdef 实现了 XDef 定义的自举,也就是用自身定义自身,
因此,xdef.xdef 便为 XDef 的元模型。
在上面的代码中,在名字空间 xdef 中的标签和属性是在定义和约束其他 XDef 的结构,
而在名字空间 meta 中的标签和属性,则是在定义和约束 xdef.xdef 自身的结构。
所以,meta 名字空间的属性的值都是一个具体的值,而
xdef 名字空间的属性的值则都是类型名,用于表示在其他 XDef 中,
该属性的值必须是该类型名所代表的类型。
故而,我们仅需要关注 xdef 名字空间的标签和属性即可,只有这些才是在其他
XDef 定义中可以出现的配置项。
这里仅对 XDef 自身的以下标签和属性做简单解释:
- 根节点
<meta:unknown-tag />表示其他 XDef 的根节点可以是任意名字, 但一定不能为xdef:unknown-tag,必须是一个确定意义的名字 <meta:define />表示为xdef.xdef自身定义一个结构可复用的节点,并通过meta:name为其命名,这样在xdef.xdef的其他节点上便可以通过meta:ref来引用该节点的结构,从而实现内部结构的复用,避免重复定义,也能够实现循环定义xdef.xdef的根节点复用了XDefNode节点,因此,在其他 XDef 的根节点上可以设置xdef:body-type等配置项
meta:unique-attr与 xdef:unique-attr 的作用相同,只是在这里,其表示在其他 XDef 中<xdef:define />可以在任意节点出现一次或多次,并且以该节点上的xdef:name属性值作为唯一定位meta:value用于配置其所在节点的子节点类型,比如<xdef:pre-parse meta:value="xpl" />,在其他 XDef 中定义<xdef:pre-parse />时,其子节点将被按照 xpl 类型进行解析
示例
定义如下结构的工作流 DSL:
<workflow
xmlns:x="/nop/schema/xdsl.xdef" x:schema="/nop/schema/xdef.xdef"
name="!string"
>
<xdef:define xdef:name="WorkflowStepModel"
id="!string" displayName="string"
joinType="enum:io.nop.wf.core.model.WfJoinType" next="string"
>
<source xdef:value="xpl" />
</xdef:define>
<steps xdef:body-type="list" xdef:key-attr="id">
<step xdef:ref="WorkflowStepModel" id="!string" />
</steps>
</workflow>
属性
属性类型
可参考《注册并使用属性类型》自定义不同领域的属性类型。
在 XDef 文件中的所有属性的值都是 def-type
类型(对应解析器 SimpleStdDomainHandlers.DefTypeType),
其内容格式为 (!~#)?{stdDomain}(:{options})?(={defaultValue})?,如,
!dict:core/some-dict=dict1,其各部分的意义如下:
!~#为属性修饰符,修饰符可根据需要同时使用多个!表示该属性必填~表示该属性为内部属性或者已经被废弃#表示可以使用编译期表达式,也就是该属性可以赋值#{xxx}形式,以支持获取编译期注入的xxx变量值
{stdDomain}为在StdDomainRegistry中注册的数据域的名字,如 dict,具体值详见以XDefConstants#STD_DOMAIN_开头的常量,使用手册见《标准数据域》{options}为传递给解析器接口IStdDomainHandler#getGenericType/IStdDomainHandler#parseProp的参数,如enum:xxx.yyy,其解析器EnumStdDomainHandler将通过{options}来得到对应的枚举类,最终,该属性的值必须为该枚举类中的枚举项{defaultValue}为属性的缺省值
其中,{defaultValue} 作为缺省值,其可以是 @attr:{attrNames} 形式,如 @attr:name,
表示从当前节点的 name 属性中取其缺省值:
<component>
<imports xdef:body-type="list" xdef:key-attr="as">
<import
as="!var-name=@attr:name"
name="var-name"
from="!string"
/>
</imports>
</component>
{attrNames}可以是逗号分隔的多个属性名,最终的缺省值为这些属性的值以|组合而成的字符串,具体处理逻辑详见XDslValidator#addDefaultAttrValue。
注意,对 DSL 属性值的类型检查和数据转换,仅发生在调用 DslXNodeToJsonTransformer#transformToObject
时,其作用是将 XNode 转换为 DSL 模型对象(即,xdef:name
对应的 class 对象),而 DslNodeLoader#loadFromResource 只做 XNode
合并,并不校验属性值。
注:调用
GenericDslParser或DslModelParser中以parseFrom开头的接口均可以加载并解析得到 DSL 模型对象。
如果 {stdDomain} 对应的是注册的自定义类型,并且指定了缺省值 {defaultValue},
则需要相应地为该自定义类型注册类型转换器,否则,
当从 XNode 转换到 DSL 模型对象,并向属性设置缺省值时,会发生类型转换异常。
若确定不需要设定缺省值,则可以不用注册类型转换器。
xdef:name
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:name | 否 | |
为 xdef 片段 <xdef:define/> 命名,其他节点可以通过 xdef:ref 来引用该片段。若是在其他节点中,则其将作为 Java 类名,在生成节点的 class 模型时会根据它和根节点上的 xdef:bean-package 设置自动生成 xdef:bean-class 属性,并进而创建对应的 class 文件。
注意:若未设置 | ||
xdef:ref
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:ref | 否 | |
用于引用当前文件中定义的 xdef 片段或者外部 xdef 定义。
引用相当于是继承已有定义(生成的 class 模型采用
/nop/schema/wf/wf.xdef /nop/schema/query/query.xdef 注意:通过 x:extends 可做差量,而
| ||
xdef:body-type
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:body-type |
| 否 |
可选值如下:
若在当前节点上设置 而若为 注意:若在该节点中定义了多类子节点( 注意:若需要为配置了 | ||
xdef:value
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:value | 否 | |
xdef:key-attr
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:key-attr | 否 | |
在当前节点被设置为 list 类型时( /nop/schema/xmeta.xdef
在生成的 class 模型中,该节点的子节点将被放在一个 _ObjMetaImpl.java 在 DSL 中便可通过 _NopAuthUser.xmeta NopAuthUser.xmeta 注意:对于非集合类型的节点,需要通过 xdef:unique-attr 唯一定位其同名标签子节点。 | ||
xdef:unique-attr
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:unique-attr | 否 | |
当非集合类型的节点下允许有多个同名标签时,
需通过 /nop/schema/biz/xbiz.xdef
在生成的 class 模型中,所有该类型的节点都会被放在一个 _BizActionModel.java 在 DSL 中便可通过 NopRuleDefinition.xbiz ExtNopRuleDefinition.xmeta 注意:对于集合类型的节点,需要通过 xdef:key-attr 唯一定位其同名标签子节点。 | ||
xdef:allow-multiple
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:allow-multiple | 否 | |
是否允许出现多个与当前节点的标签同名的兄弟节点: /nop/schema/query/filter.xdef 若是不允许重复同名标签,则将以标签名作为节点的定位坐标做差量运算,
否则,将以 注意:若当前节点配置了 xdef:unique-attr,
或者其父节点配置了 xdef:key-attr,
则该节点的 | ||
xdef:unknown-attr
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:unknown-attr | 否 | |
在 DSL 节点的属性名不确定但类型可确定时,可通过该配置项定义这些属性: 注意:名称不确定但类型确定的属性集合只能定义一个, 也就是,不能定义多种类型的非具名属性。 若是需要定义多个类型的非具名属性,则可以通过固定的标签名进行定义,例如: 注意:通过配置 xdef:bean-unknown-attrs-prop 可以指定在 DSL 节点上的非具名属性放在模型对象的哪个属性中。 | ||
xdef:transformer-class
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:transformer-class | 否 | |
[仅根节点] 在加载得到 DSL 的 XNode 根节点之后,将调用这个列表中的转换器进行格式转换, 可以通过转换器转换得到标准格式,或者执行版本迁移等。 转换器为
例如,通过 task.xdef common.task.xml | ||
xdef:support-extends
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:support-extends | 否 | |
除了在 DSL 文件的根节点上可以写 x:extends
表示可以从指定的基础模型继承之外,在子节点上也可以使用 从根节点的 | ||
xdef:default-extends
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:default-extends | 否 | |
[仅根节点] 缺省的 x:extends 模型文件。
如果非空,则由此 XDef 文件描述的模型文件中,总是会缺省继承
例如,在 /nop/schema/biz/xbiz.xdef 则可以在 /nop/core/defaults/default.xbiz 注意:若是部分 DSL 不需要继承 MyBiz.xbiz 注意:在 xdef:ref 引用的 XDef 中所配置的
component.xdef page.xdef | ||
xdef:bean-class
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-class | 否 | |
指定节点所生成的 class 模型的全名称(= 包名 + 类名)。 默认由 xdef:bean-package 和 xdef:name 组合而成。 | ||
xdef:bean-package
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-package | 否 | |
[仅根节点] 指定根节点及其子节点所生成的 class 模型的包名。 注意:只要配置了 | ||
xdef:bean-extends-type
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-extends-type | 否 | |
指定节点所生成的 class 模型的父类全名称(= 包名 + 类名): /nop/schema/xlib.xdef _XplTagLib.java | ||
xdef:bean-implements-types
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-implements-types | 否 | |
指定节点所生成的 class 模型所要实现的接口全名称(= 包名 + 类名)列表(逗号分隔): /nop/schema/xlib.xdef _XtRuleModel.java | ||
xdef:bean-prop
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-prop | 否 | |
在生成 class 模型时,
默认是以标签名作为模型的属性名,而对于设置了 xdef:unique-attr
的节点,则将按照 若是需要修改节点对应的 class 模型的属性名,则可设置 | ||
xdef:bean-child-name
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-child-name | 否 | |
在 xdef:body-type 节点所属的 class 模型中为其列表元素创建对应的 getter/has 方法,以通过该节点的唯一标识(标签名或者 xdef:unique-attr、xdef:key-attr 指定的属性值)获取其对象: /nop/schema/xlib.xdef _XplTagLib.java 需要特别注意的是,如果 xdef:body-type
为 则 显然, 为了纠正该问题,需要通过 xdef:define
定义单独的 xdef 片段,再在 xdef:ref 引用时,单独设置
由于 | ||
xdef:bean-ref-prop
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-ref-prop | 否 | |
注:用途暂不明确。 | ||
xdef:bean-body-prop
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-body-prop | 否 | |
在为节点生成 class 模型时,
默认会将其子节点放在名为 /nop/schema/orm/sql-lib.xdef 以上配置生成的 class 模型如下: 此配置对于设置了 xdef:body-type 的节点同样有效: /nop/schema/orm/entity.xdef
注意:若是未设置 xdef:name,即不为该节点生成 class 模型,则不能为其配置
注意:对于设置了 因为,在 IXDefNode#isSimple 通过在根节点上显式设置 | ||
xdef:bean-body-type
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-body-type | 否 | |
为配置了 xdef:body-type 的节点的 class 模型中的 xdef:bean-body-prop 属性显式指定数据类型: /nop/schema/beans.xdef 主要用于指定属性的泛型类型,以支持接受其不同子类的子节点模型对象。 | ||
xdef:bean-tag-prop
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-tag-prop | 否 | |
在 JSON 序列化时,会根据 注意:对于配置了 xdef:body-type
的节点,若其定义了多类子节点( 注意:若是需要在子节点对应的模型对象属性上记录标签名,则必须在子节点上配置
| ||
xdef:bean-sub-type-prop
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-sub-type-prop | 否 | |
在 JSON 反序列化时,将根据 注意:对于配置了 xdef:body-type
的节点,若其定义了多类子节点( 注意:若是需要在子节点对应的模型对象属性上记录标签名,则必须在子节点上配置 xdef:bean-tag-prop。 | ||
xdef:bean-unknown-attrs-prop
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-unknown-attrs-prop | 否 | |
对于配置了 xdef:unknown-attr
且生成了 class 模型的节点,
可以同时配置 例如: button.xdef 所生成的 class 模型为: 通过 button.xml | ||
xdef:bean-unknown-children-prop
| 名称 | 类型 | 必填? |
|---|---|---|
| xdef:bean-unknown-children-prop | 否 | |
节点
xdef:define
| 名称 | 类型 | 必须? |
|---|---|---|
| xdef:define |
| 否 |
定义 xdef 片段,以便于复用节点结构: /nop/schema/orm/entity.xdef 与普通的 xdef:name 节点不同的是, 注意:不能在 | ||
xdef:pre-parse
| 名称 | 类型 | 必须? |
|---|---|---|
| xdef:pre-parse | 否 | |
在调用 xview.xdef 其发生在 DSL 节点合并之后,但在构造生成模型对象之前。
注意:仅在直接以 注意:可参考以下代码为 | ||
xdef:post-parse
| 名称 | 类型 | 必须? |
|---|---|---|
| xdef:post-parse | 否 | |
在调用 xview.xdef 其发生在 DSL 节点合并且已生成模型对象之后。
注意:仅在直接以 注意:可参考以下代码为 注意:该脚本的最后一条赋值或 | ||
附录
生成节点 class 模型
public class SchemaCodeGen {
public static void main(String[] args) {
AppConfig.getConfigProvider()
.updateConfigValue(CoreConfigs.CFG_CORE_MAX_INITIALIZE_LEVEL,
CoreConstants.INITIALIZER_PRIORITY_ANALYZE);
CoreInitialization.initialize();
try {
run();
} finally {
CoreInitialization.destroy();
}
}
public static void run() {
File projectDir = MavenDirHelper.projectDir(SchemaCodeGen.class);
// 运行项目根目录下 precompile 目录中的 *.xgen 脚本:目录必须存在,但可为空目录
XCodeGenerator.runPrecompile(projectDir, "/", false);
// 运行项目根目录下 precompile2 目录中的 *.xgen 脚本:目录必须存在,但可为空目录
XCodeGenerator.runPrecompile2(projectDir, "/", false);
// 运行项目根目录下 postcompile 目录中的 *.xgen 脚本:目录必须存在,但可为空目录
XCodeGenerator.runPostcompile(projectDir, "/", false);
}
}
<c:script xmlns:c="c"><![CDATA[
codeGenerator.renderModel('/vfs/to/schema/xxx.xdef', '/nop/templates/xdsl', '/', $scope);
]]></c:script>
注册并使用属性类型
首先,注册 Nop 模块初始化器 ICoreInitializer 的实现类,如 XuiCoreInitializer:
io.nop.xui.initialize.XuiCoreInitializer
io.nop.core.initialize.ICoreInitializer为固定的文件名,其最终由java.util.ServiceLoader#load加载,在该文件内可以放置多行ICoreInitializer的实现类的全名。
在 ICoreInitializer 的实现类 XuiCoreInitializer 中调用
StdDomainRegistry#registerStdDomainHandler 以注册自定义属性类型的解析器,如
VueNodeStdDomainHandler:
public class XuiCoreInitializer implements ICoreInitializer {
private final Cancellable cancellable = new Cancellable();
@Override
public int order() {
return CoreConstants.INITIALIZER_PRIORITY_REGISTER_XLANG;
}
@Override
public void initialize() {
IStdDomainHandler handler = VueNodeStdDomainHandler.INSTANCE;
StdDomainRegistry registry = StdDomainRegistry.instance();
registry.registerStdDomainHandler(handler);
cancellable.appendOnCancelTask(() -> {
registry.unregisterStdDomainHandler(handler);
});
}
@Override
public void destroy() {
cancellable.cancel();
}
}
最后,实现自定义属性类型解析器 VueNodeStdDomainHandler 即可:
public class VueNodeStdDomainHandler extends SimpleStdDomainHandlers.XmlType {
public static final VueNodeStdDomainHandler INSTANCE = new VueNodeStdDomainHandler();
@Override
public String getName() {
return "vue-node";
}
/**
* 根据 options 确定属性类型对应的运行环境中的数据类型(含自定义 class),
* 如 String、Date 等,可直接引用定义在 PredefinedGenericTypes
* 上的内置类型,或者通过 ReflectionManager 构建
*/
@Override
public IGenericType getGenericType(
boolean mandatory, String options
) {
return ReflectionManager.instance().buildRawType(VueNode.class);
}
/**
* 根据 options 解析标签属性(对应参数 propName)的值(对应参数 text)。
* 对标签的文本子节点的解析,则也调用该方法
*/
@Override
public Object parseProp(
String options, SourceLocation loc,
String propName, Object text,
XLangCompileTool cp
) {
return null;
}
/**
* 若是通过 xdef:value 设置标签内部的非文本节点类型,
* 则在该方法内将其子节点(对应参数 body)转换为目标类型的对象结构
*/
@Override
public Object parseXmlChild(
String options, XNode body,
XLangCompileTool cp
) {
return new VueTemplateParser().parseTemplate(body);
}
}
各种类型的转换处理逻辑可以参考
io.nop.xlang.xdef.domain.SimpleStdDomainHandler的实现类。
这样,便可以在定义 XDef 时,为相关属性指定其类型为 vue-node 了:
<component ...>
<!-- ... -->
<template xdef:value="vue-node" />
</component>
注册类型转换器
类型转换器 ITypeConverter 的作用是在调用 IBeanModel#setProperty
动态为模型属性赋值时,自动将赋值参数转换为 setter 接口的参数类型,
从而保证数据转换的准确性。
假设注册了一个自定义类型 XuiSize:
public class XuiSize {
public enum Unit {
base('u'), // 基本单元尺寸,如 12u
percent('%'), // 百分比尺寸,如 50%
}
public static XuiSize parse(
Object value, Function<ErrorCode, NopException> errorFactory
) {
// 将 value 转换为 XuiSize
}
}
则首先为其创建一个 ITypeConverter 静态变量:
public class XuiSize {
public static final ITypeConverter TYPE_CONVERTER = (value, errorFactory) -> {
if (value instanceof XuiSize) {
return value;
}
return parse(value, errorFactory);
};
}
再在其所在 Nop 模块的初始化器 ICoreInitializer 中注册该转换器:
public class XuiCoreInitializer implements ICoreInitializer {
private final Cancellable cancellable = new Cancellable();
@Override
public void initialize() {
// 其他初始化逻辑
SysConverterRegistry registry = SysConverterRegistry.instance();
registry.registerConverter("toXuiSize", XuiSize.class, XuiSize.TYPE_CONVERTER);
this.cancellable.appendOnCancelTask(() -> {
registry.unregisterTypeConverter("toXuiSize", XuiSize.class, XuiSize.TYPE_CONVERTER);
});
}
}