跳到主要内容

XDef

提示
  • Nop 平台还处于开发阶段, 本文档中的实践方案可能会部分失效,但本人精力有限,无法及时跟进,请自行按照最新代码调整;
  • 您可以与 DeepWiki 进行问答互动(支持中文)以深入学习 Nop 平台的设计与实现;
  • 若此文对您有很大帮助,请投币支持一下吧;
版权声明
阅读提醒
本文还在编辑整理中,会存在较多错误,请谨慎阅读!

在 Nop 中,所有的 DSL 均需要由与其对应的 XDef 模型来定义和约束其结构,该 XDef 模型即为 DSL 的元模型。

对 XDef 结构的定义(即,元模型的元模型)见 /nop/schema/xdef.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-attrxdef: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 属性中取其缺省值:

/nop/schema/xui/simple-component.xdef
<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 合并,并不校验属性值。

注:调用 GenericDslParserDslModelParser 中以 parseFrom 开头的接口均可以加载并解析得到 DSL 模型对象。

如果 {stdDomain} 对应的是注册的自定义类型,并且指定了缺省值 {defaultValue}, 则需要相应地为该自定义类型注册类型转换器,否则, 当从 XNode 转换到 DSL 模型对象,并向属性设置缺省值时,会发生类型转换异常。

若确定不需要设定缺省值,则可以不用注册类型转换器。

xdef:name

名称类型必填?
xdef:name

var-name

为 xdef 片段 <xdef:define/> 命名,其他节点可以通过 xdef:ref 来引用该片段。若是在其他节点中,则其将作为 Java 类名,在生成节点的 class 模型时会根据它和根节点上的 xdef:bean-package 设置自动生成 xdef:bean-class 属性,并进而创建对应的 class 文件。

生成 class 的节点将包括所有配置了 xdef:name 的节点,也包含 <xdef:define/>

注意:若未设置 xdef:bean-packagexdef:bean-class, 则不会生成节点的 class 模型。

xdef:ref

名称类型必填?
xdef:ref

xdef-ref

用于引用当前文件中定义的 xdef 片段或者外部 xdef 定义。 引用相当于是继承已有定义(生成的 class 模型采用 extends 派生)。如果再增加属性或者子节点则表示在已有定义基础上扩展:

/nop/schema/wf/wf.xdef
<workflow ...>
<xdef:define xdef:name="WorkflowStepModel" ...>
...
</xdef:define>

<steps xdef:body-type="list" xdef:key-attr="id">
<step xdef:ref="WorkflowStepModel" id="!string" />
</steps>
</workflow>
/nop/schema/query/query.xdef
<query x:schema="/nop/schema/xdef.xdef"
xmlns:x="/nop/schema/xdsl.xdef"
xmlns:xdef="/nop/schema/xdef.xdef"
>
<filter xdef:ref="filter.xdef"/>
<orderBy xdef:ref="order-by.xdef"/>
<groupBy xdef:ref="group-by.xdef"/>
</query>

注意:通过 x:extends 可做差量,而 xdef:ref 只能做增量。

xdef:body-type

名称类型必填?
xdef:body-type

io.nop.xlang.xdef.XDefBodyType

可选值如下:

  • union: 在 DSL 中最多只允许出现一个子节点
  • list: 将当前节点解析为 List。若在当前节点上指定了 xdef:key-attr, 则该节点解析后对应 KeyedList 类型,可通过 xdef:key-attr 指定的子节点属性的值作为 key
  • map: 将当前节点解析为 Map,且以子节点的标签名作为 key

若在当前节点上设置 xdef:body-type="map",则将按照标签名对子节点做 x:prototype 扩展(节点间的继承), 也即,x:prototype 所引用的名字需为目标节点的标签名:

<!-- xlib 的 xdef 定义 -->
<lib>
<tags xdef:body-type="map">
<xdef:unknown-tag ... />
</tags>
</lib>

<!-- xlib 的 dsl 定义 -->
<lib>
<tags>
<DoSomething />

<DoOtherthing x:prototype="DoSomething" />
</tags>
</lib>

而若为 list,则需要按照当前节点上 xdef:key-attr 指定的子节点的属性值作为扩展目标的引用名字:

<!-- xmeta 的 xdef 定义 -->
<xmeta>
<props xdef:body-type="list" xdef:key-attr="name">
<prop name="!prop-path" />
</props>
</xmeta>

<!-- xmeta 的 dsl 定义 -->
<xmeta>
<props>
<prop x:abstract="true" name="base-prop" />

<prop name="some" x:prototype="base-prop" />
</props>
</xmeta>

注意:若在该节点中定义了多类子节点(union 类型不管子节点数量),则必须配置 xdef:bean-sub-type-propxdef:bean-tag-prop, 用以在其指定的扩展属性(如上例中的 type)上记录标签名,确保可将模型对象转换为 XNode

注意:若需要为配置了 xdef:body-type 但没有自定义属性(既没有定义确定名字的属性,也没有配置 xdef:unknown-attr)的节点(可以为根节点生成 class 模型, 则需要在该节点上同时配置 xdef:bean-body-prop

xdef:value

名称类型必填?
xdef:value

def-type

xdef:key-attr

名称类型必填?
xdef:key-attr

xml-name

在当前节点被设置为 list 类型时(xdef:body-type="list"), 需要同时设置 xdef:key-attr 以指定用于唯一定位其子节点的节点属性

/nop/schema/xmeta.xdef
<meta xdef:name="ObjMetaImpl">
<props
xdef:body-type="list"
xdef:key-attr="name"
>
<prop xdef:name="ObjPropMetaImpl"
name="!prop-path"
/>
</props>
</meta>
  • xdef:key-attr 指定的是其所在节点的子节点上的属性名;
  • xdef:body-type="list" 中的子节点可以是不同名字的标签;

在生成的 class 模型中,该节点的子节点将被放在一个 KeyedList 类型的属性中:

_ObjMetaImpl.java
public abstract class _ObjMetaImpl extends ObjSchemaImpl {
private KeyedList<ObjPropMetaImpl> _props = KeyedList.emptyList();

public java.util.List<ObjPropMetaImpl> getProps() {
return _props;
}
}

在 DSL 中便可通过 xdef:key-attr 指定属性的值对子节点做差量运算:

_NopAuthUser.xmeta
<meta ...>
<props>
<prop name="userName" ... />
<prop name="email" ... />
</props>
</meta>
NopAuthUser.xmeta
<meta ... x:extends="_NopAuthUser.xmeta">
<props>
<prop name="userName" allowFilterOp="eq,contains" ui:filterOp="contains"/>
<prop name="email" x:override="remove" />
</props>
</meta>

注意:对于非集合类型的节点,需要通过 xdef:unique-attr 唯一定位其同名标签子节点。

xdef:unique-attr

名称类型必填?
xdef:unique-attr

xml-name

当非集合类型的节点下允许有多个同名标签时, 需通过 xdef:unique-attr 指定用于唯一定位这些同名标签节点的节点属性

/nop/schema/biz/xbiz.xdef
<biz ...>
<actions ...>
<query xdef:name="BizActionModel">
<arg xdef:name="BizActionArgModel"
xdef:unique-attr="name"
name="!var-name" />
</query>
</actions>
</biz>

xdef:unique-attr 指定的是其所在节点上的属性名。

在生成的 class 模型中,所有该类型的节点都会被放在一个 KeyedList 类型的属性中:

_BizActionModel.java
public abstract class _BizActionModel extends AbstractComponentModel {
private KeyedList<BizActionArgModel> _args = KeyedList.emptyList();

public List<BizActionArgModel> getArgs() {
return _args;
}
}

在 DSL 中便可通过 xdef:unique-attr 指定属性的值对子节点做差量运算:

NopRuleDefinition.xbiz
<biz ...>
<actions>
<query name="getOutputFieldsEditSchema">
<arg name="ruleId" mandatory="true" type="String" />
</query>
</actions>
</biz>
ExtNopRuleDefinition.xmeta
<biz ... x:extends="NopRuleDefinition.xbiz">
<actions>
<query name="getOutputFieldsEditSchema">
<arg name="ruleId" mandatory="false" />
</query>
</actions>
</biz>

注意:对于集合类型的节点,需要通过 xdef:key-attr 唯一定位其同名标签子节点。

xdef:allow-multiple

名称类型必填?
xdef:allow-multiple

boolean

是否允许出现多个与当前节点的标签同名的兄弟节点:

/nop/schema/query/filter.xdef
<filter ...>
<xdef:define ...>
<lt name="!string"
xdef:allow-multiple="true"
... />
</xdef:define>
</filter>

若是不允许重复同名标签,则将以标签名作为节点的定位坐标做差量运算, 否则,将以 xdef:unique-attrxdef:key-attr 指定属性的值作为节点的定位坐标。

注意:若当前节点配置了 xdef:unique-attr, 或者其父节点配置了 xdef:key-attr, 则该节点的 xdef:allow-multiple 的缺省值为 true

xdef:unknown-attr

名称类型必填?
xdef:unknown-attr

def-type

在 DSL 节点的属性名不确定但类型可确定时,可通过该配置项定义这些属性:

<!-- styles 的 xdef 定义 -->
<styles xdef:body-type="map">
<xdef:unknown-tag xdef:unknown-attr="enum:StyleAttrType" />
</styles>

<!-- styles 的 dsl 定义 -->
<styles>
<border-left size="Size" color="Color" />
</styles>

注意:名称不确定但类型确定的属性集合只能定义一个, 也就是,不能定义多种类型的非具名属性。 若是需要定义多个类型的非具名属性,则可以通过固定的标签名进行定义,例如:

  <props xdef:body-type="list" xdef:key-attr="name">
<prop name="!prop-path" type="generic-type" />
</props>

注意:通过配置 xdef:bean-unknown-attrs-prop 可以指定在 DSL 节点上的非具名属性放在模型对象的哪个属性中。

xdef:transformer-class

名称类型必填?
xdef:transformer-class

class-name-set

[仅根节点] 在加载得到 DSL 的 XNode 根节点之后,将调用这个列表中的转换器进行格式转换, 可以通过转换器转换得到标准格式,或者执行版本迁移等。

转换器为 IXNodeTransformer 的实现类,且多个转换器为串联的, 即,前一个的转换结果将作为下一个转换器的参数。

转换器的解析和调用逻辑详见 io.nop.xlang.xdsl.XDslExtender#transformNode

例如,通过 io.nop.xlang.xdsl.transformer.InOutNodeTransformer 可以自动识别标签中 in:out: 前缀标记的属性,并把它们自动转换为 inputoutput 子节点:

task.xdef
<task xdef:transformer-class="io.nop.xlang.xdsl.transformer.InOutNodeTransformer"
...>
<steps>
<xpl ... />
</steps>
</task>
common.task.xml
<task>
<steps>
<xpl name="step1" in:x="1" out:RESULT="x+y">
<in:y mandatory="true">2</in:y>
</xpl>

<!-- 等价于 -->
<xpl name="step1">
<input name="x">
<source>1</source>
</input>
<input name="y" mandatory="true">
<source>2</source>
</input>

<output name="RESULT">
<source> x + y</source>
</output>
</xpl>
</steps>
</task>

xdef:support-extends

名称类型必填?
xdef:support-extends

boolean

除了在 DSL 文件的根节点上可以写 x:extends 表示可以从指定的基础模型继承之外,在子节点上也可以使用 x:extends。 需要在子节点的 XDef 元模型上配置 xdef:support-extends=true 才允许该子节点使用 x:extends 机制。例如:

<forms x:extends="base.forms.xml">
<form id="add" x:extends="default.form.xml" />
</forms>

从根节点的 base.forms.xml 基础模型中我们有可能继承得到一个 add 表单, 同时我们又通过 x:extends 指定了 add 表单从 default.form.xml 继承。 而在 default.form.xml 中,它可能继续使用 x:extends 机制从其他文件继承。 如果完整的考虑所有继承节点的情况,则合并算法的实现会变得相当复杂, 所以在 Nop 平台的 Delta 合并算法中我们做了一点简化,规定如果节点上明确设置了 x:extends,则会自动忽略从根节点上继承得到的节点内容。例如上面的例子中, 从 base.forms.xml 中继承得到的 add form 会被自动忽略。

xdef:default-extends

名称类型必填?
xdef:default-extends

v-path

[仅根节点] 缺省的 x:extends 模型文件。 如果非空,则由此 XDef 文件描述的模型文件中,总是会缺省继承 xdef:default-extends 所指定的模型文件,通过此机制可以引入全局模型假定,简化模型配置。

注意,与 x:extends 不同的是,该属性值只能为单一的 v-path 路径, 且该路径对应的资源文件也可以不存在。

例如,在 xbiz.xdef 中默认设置了 xdef:default-extends

/nop/schema/biz/xbiz.xdef
<biz
xdef:default-extends="/nop/core/defaults/default.xbiz"
...>
<!-- ... -->
</biz>

则可以在 /nop/core/defaults/default.xbiz 中配置后处理函数 biz-gen:DefaultBizPostExtends,从而确保所有的 XBiz 模型均会执行相同的后处理:

/nop/core/defaults/default.xbiz
<biz ...>
<x:post-extends>
<biz-gen:DefaultBizPostExtends xpl:lib="/nop/core/xlib/biz-gen.xlib"/>
</x:post-extends>

<!-- ... -->
</biz>

注意:若是部分 DSL 不需要继承 xdef:default-extends 所指定的模型, 则需要在 x:extends 中包含 none

MyBiz.xbiz
<biz x:extends="none,/my/biz/base.xbiz"
...>
<!-- ... -->
</biz>

注意:在 xdef:ref 引用的 XDef 中所配置的 xdef:default-extends 不会继承给当前的 XDef,需要显式指定:

component.xdef
<component ...
xdef:default-extends="/path/to/component/default.xui"
>
</component>
page.xdef
<page ...
xdef:ref="component.xdef"
xdef:default-extends="/path/to/component/default.xui"
>
<!-- Note: 页面本质上也是一个 UI 组件,完全复用组件的结构即可 -->
</page>

xdef:bean-class

名称类型必填?
xdef:bean-class

class-name

指定节点所生成的 class 模型的全名称(= 包名 + 类名)。 默认由 xdef:bean-packagexdef:name 组合而成。

xdef:bean-package

名称类型必填?
xdef:bean-package

package-name

[仅根节点] 指定根节点及其子节点所生成的 class 模型的包名。

注意:只要配置了 xdef:bean-package,则所有配置了 xdef:name 的节点(包括 <xdef:define/>)均会生成 class 模型。

xdef:bean-extends-type

名称类型必填?
xdef:bean-extends-type

generic-type

指定节点所生成的 class 模型的父类全名称(= 包名 + 类名):

/nop/schema/xlib.xdef
<lib ...
xdef:name="XplTagLib"
xdef:bean-extends-type="io.nop.xlang.xdsl.AbstractDslModel"
>
<!-- ... -->
</lib>
_XplTagLib.java
public class _XplTagLib extends io.nop.xlang.xdsl.AbstractDslModel {
// ...
}

xdef:bean-implements-types

名称类型必填?
xdef:bean-implements-types

generic-type-list

指定节点所生成的 class 模型所要实现的接口全名称(= 包名 + 类名)列表(逗号分隔):

/nop/schema/xlib.xdef
  <xdef:define
xdef:name="XtRuleModel"
xdef:bean-implements-types="io.nop.xlang.xt.model.IXtRuleModel"
/>
_XtRuleModel.java
public class _XtRuleModel ... implements io.nop.xlang.xt.model.IXtRuleModel {
// ...
}

xdef:bean-prop

名称类型必填?
xdef:bean-prop

prop-name

生成 class 模型时, 默认是以标签名作为模型的属性名,而对于设置了 xdef:unique-attr 的节点,则将按照 节点名驼峰变换 + 's' 的形式命名,比如,<task-step .../> 节点对应的属性名为 taskSteps

若是需要修改节点对应的 class 模型的属性名,则可设置 xdef:bean-prop 以自定义该属性名:

  <task-step
xdef:unique-attr="id"
xdef:bean-prop="taskStepList"
... />

xdef:bean-child-name

名称类型必填?
xdef:bean-child-name

var-name

xdef:body-type 节点所属的 class 模型中为其列表元素创建对应的 getter/has 方法,以通过该节点的唯一标识(标签名或者 xdef:unique-attrxdef:key-attr 指定的属性值)获取其对象:

/nop/schema/xlib.xdef
<lib xdef:name="XplTagLib">
<tags xdef:body-type="map" xdef:bean-child-name="tag">
<xdef:unknown-tag xdef:name="XplTag" ... />
</tags>
</lib>
_XplTagLib.java
public class _XplTagLib extends AbstractDslModel {
private Map<String, XplTag> _tags = Collections.emptyMap();

public XplTag getTag(String name){
return this._tags.get(name);
}

public boolean hasTag(String name){
return this._tags.containsKey(name);
}
}

需要特别注意的是,如果 xdef:body-typelist,且其节点本身也要生成 class 模型(由 xdef:name 指定名称),如:

  <xdef:define xdef:name="Choose">
<when xdef:name="When"
xdef:unique-attr="name" name="!conf-name"

xdef:body-type="list" xdef:key-attr="bid"
xdef:bean-body-prop="blocks"
xdef:bean-child-name="block"
test="!expr"
>
<xdef:unknown-tag xdef:name="WhenBlock" bid="!conf-name" />
</when>
<otherwise xdef:name="Otherwise" />
</xdef:define>

xdef:bean-child-name 的设置会同时作用在 _Choose_When 两个模型的 getter/has 方法上,得到如下代码:

public abstract class _Choose ... {
private KeyedList<When> whens = KeyedList.emptyList();

public When getBlock() { ... } // 注意,预期应该生成 getWhen()
}

public abstract class _When ... {
private KeyedList<WhenBlock> blocks = KeyedList.emptyList();

public WhenBlock getBlock() { ... }
}

显然,_Choose#getBlock() 与预期得到的 _Choose#getWhen() 是不相符的。

为了纠正该问题,需要通过 xdef:define 定义单独的 xdef 片段,再在 xdef:ref 引用时,单独设置 xdef:bean-child-name 以覆盖在 When 节点上的该配置:

  <xdef:define xdef:name="When"
xdef:body-type="list" xdef:key-attr="bid"
xdef:bean-body-prop="blocks"
xdef:bean-child-name="block"
>
<xdef:unknown-tag xdef:name="WhenBlock" bid="!conf-name" />
</xdef:define>

<xdef:define xdef:name="Choose">
<when xdef:ref="When"
xdef:bean-child-name="when"
xdef:unique-attr="name" name="!conf-name"
test="!expr"
/>
<otherwise xdef:name="Otherwise" />
</xdef:define>

由于 ChooseWhen 的模型代码生成是独立的,因此,对 xdef:bean-child-name 的覆盖配置不会影响 When 模型的生成,从而可确保二者生成的代码是正确的。

xdef:bean-ref-prop

名称类型必填?
xdef:bean-ref-prop

prop-name

:用途暂不明确。

xdef:bean-body-prop

名称类型必填?
xdef:bean-body-prop

prop-name

在为节点生成 class 模型时, 默认会将其子节点放在名为 body 的属性上。若是需要指定其他名字, 则可以在该节点上配置 xdef:bean-body-prop

/nop/schema/orm/sql-lib.xdef
<sql-lib>
<fragments ...>
<fragment id="!xml-name"
xdef:value="xpl-sql"
xdef:bean-body-prop="source"
xdef:name="SqlFragmentModel"
/>
</fragments>
</sql-lib>

以上配置生成的 class 模型如下:

public abstract class _SqlFragmentModel ... {
private io.nop.core.lang.sql.ISqlGenerator _source;
}

此配置对于设置了 xdef:body-type 的节点同样有效:

/nop/schema/orm/entity.xdef
<entity>
<components ...>
<component
xdef:name="OrmComponentModel"
xdef:body-type="list" xdef:key-attr="name"
xdef:bean-body-prop="props"
...>
<prop xdef:name="OrmComponentPropModel"
name="!var-name"
... />
</component>
</components>
</entity>
public abstract class _OrmComponentModel ... {
private KeyedList<OrmComponentPropModel> _props = KeyedList.emptyList();
}

对配置了 xdef:body-type 节点的子节点的模型转换逻辑详见 XDefToObjMeta#bodyToProp

注意:若是未设置 xdef:name,即不为该节点生成 class 模型,则不能为其配置 xdef:bean-body-prop,否则,在生成根节点的 class 模型时将会报异常 params={expr=prop.schema!.type!},desc=值不允许为null

注意:对于设置了 xdef:body-type 且没有自定义属性(既没有定义确定名字的属性,也没有配置 xdef:unknown-attr)的节点(包括根节点), 若要为其生成 class 模型, 则必须显式配置 xdef:bean-body-prop,即使其值就是 body

因为,在 DslXNodeToJsonTransformer#transformToObject 中会根据 IXDefNode#isSimple 的结果来决定如何解析当前节点。若其为 true,则将按 MapList 类型来解析当前节点,得到的是集合类型的对象。而若其为 false,才按照在节点上指定的 class 来解析当前节点:

IXDefNode#isSimple
  default boolean isSimple() {
return !hasAttr()
&& (getXdefBodyType() != null || getXdefValue() != null)
&& getXdefBeanTagProp() == null
&& getXdefBeanBodyProp() == null
&& getXdefBeanCommentProp() == null;
}

通过在根节点上显式设置 xdef:bean-body-prop 便可以让 IXDefNode#isSimple 始终返回 false,从而避免 DslModelParser 在加载 DSL 模型时出现类型 cast 异常。

xdef:bean-body-type

名称类型必填?
xdef:bean-body-type

generic-type

为配置了 xdef:body-type 的节点的 class 模型中的 xdef:bean-body-prop 属性显式指定数据类型:

/nop/schema/beans.xdef
<beans>
<xdef:define
xdef:name="BeanPropValue"
xdef:body-type="union"
xdef:bean-body-prop="body"
xdef:bean-body-type="io.nop.ioc.model.IBeanPropValue"
...>
<bean ... />
<ref ... />
<list ... />
<map ... />
...
</xdef:define>

<xdef:define
xdef:name="BeanCollectionValue"
xdef:body-type="list"
xdef:bean-body-prop="body"
xdef:bean-body-type="List&lt;io.nop.ioc.model.IBeanPropValue>"
...>
...
</xdef:define>
</beans>

主要用于指定属性的泛型类型,以支持接受其不同子类的子节点模型对象。

xdef:bean-tag-prop

名称类型必填?
xdef:bean-tag-prop

prop-name

在 JSON 序列化时,会根据 xdef:bean-tag-prop 设置的名称作为对象的属性名, 并将该配置所在的节点的标签名作为该属性的值:

  <steps xdef:body-type="list" xdef:key-attr="name"
xdef:bean-sub-type-prop="type"
>
<step name="!string" xdef:bean-tag-prop="type" />
<join name="!string" xdef:bean-tag-prop="type" />
</steps>
// <steps>
// <step name="a" />
// <join name="b" />
// </steps>
// 以上 DSL 将被解析为:
{
"steps": [
{
"type": "step",
"name": "a"
},
{
"type": "join",
"name": "b"
}
]
}

注意:对于配置了 xdef:body-type 的节点,若其定义了多类子节点(union 类型不管子节点数量),则必须配置 xdef:bean-sub-type-propxdef:bean-tag-prop, 用以在其指定的扩展属性(如上例中的 type)上记录标签名,确保可将模型对象转换为 XNode

注意:若是需要在子节点对应的模型对象属性上记录标签名,则必须在子节点上配置 xdef:bean-tag-prop

xdef:bean-sub-type-prop

名称类型必填?
xdef:bean-sub-type-prop

prop-name

在 JSON 反序列化时,将根据 xdef:bean-sub-type-prop 指定的数组元素的属性来确定子节点的类型:

  <steps xdef:body-type="list" xdef:key-attr="name"
xdef:bean-sub-type-prop="type"
>
<step name="!string" xdef:bean-tag-prop="type" />
<join name="!string" xdef:bean-tag-prop="type" />
</steps>
<!-- {
"steps": [{
"type": "step",
"name": "a"
}, {
"type": "join",
"name": "b"
}]
}
以上 JSON 将被解析为:
-->
<steps>
<step name="a" />
<join name="b" />
</steps>

注意:对于配置了 xdef:body-type 的节点,若其定义了多类子节点(union 类型不管子节点数量),则必须配置 xdef:bean-sub-type-propxdef:bean-tag-prop, 用以在其指定的扩展属性(如上例中的 type)上记录标签名,确保可将模型对象转换为 XNode

注意:若是需要在子节点对应的模型对象属性上记录标签名,则必须在子节点上配置 xdef:bean-tag-prop

xdef:bean-unknown-attrs-prop

名称类型必填?
xdef:bean-unknown-attrs-prop

prop-name

对于配置了 xdef:unknown-attr生成了 class 模型的节点, 可以同时配置 xdef:bean-unknown-attrs-prop 以指定在其 DSL 节点上的非具名属性放在模型的哪个属性上。

例如:

button.xdef
  <Button xdef:name="XuiButton"
xdef:unknown-attr="any"
xdef:bean-unknown-attrs-prop="attrs"
/>

所生成的 class 模型为:

public class _XuiButton ... {
private Map<String, Object> _attrs;
}

通过 _XuiButton#_attrs 便可以得到在 <Button/> 上配置的 labelstyle 等非具名属性:

button.xml
  <Button label="Submit" style="color: blue;" />

xdef:bean-unknown-children-prop

名称类型必填?
xdef:bean-unknown-children-prop

prop-name

节点

xdef:define

名称类型必须?
xdef:define

io.nop.xlang.xdef.impl.XDefNode

定义 xdef 片段,以便于复用节点结构:

/nop/schema/orm/entity.xdef
<entity>
<xdef:define xdef:name="OrmReferenceModel" ...>
...
</xdef:define>

<relations ...>
<to-one xdef:name="OrmToOneReferenceModel"
xdef:ref="OrmReferenceModel" ...>
...
</to-one>

<to-many xdef:name="OrmToManyReferenceModel"
xdef:ref="OrmReferenceModel" ...>
...
</to-many>
</relations>
</entity>

与普通的 xdef:name 节点不同的是,xdef:define 定义的是可复用的虚拟节点,本质上是在定义 class 的基类, 因此,其不会作为属性出现在生成的 class 模型中。

注意:不能在 <xdef:define/> 中嵌套 <xdef:define/>

xdef:pre-parse

名称类型必须?
xdef:pre-parse

xpl

在调用 DslModelParser#parseFromResourceDslModelParser#parseFromNode 时执行该节点内的 Xpl 脚本:

xview.xdef
<view>
<xdef:pre-parse xmlns:c="c">
<c:script><![CDATA[
// 可访问变量 _dsl_root 以操作合并后的 DSL 根节点
]]></c:script>
</xdef:pre-parse>
</view>

其发生在 DSL 节点合并之后,但在构造生成模型对象之前。

其调用逻辑见 AbstractDslParser#runPreParse

注意:仅在直接以 xdef:pre-parse 所在 *.xdef 生成模型对象时有效,在以 xdef:ref 引入的元模型中无效。

注意:可参考以下代码为 XNode 构造模型对象

// 解析 DSL 获得 XNode 节点
XNode node = XNodeParser.instance().parseFromVirtualPath("/path/to/a.xmeta");
// 根据 DSL 的元模型解析得到模型对象
ObjMetaImpl objMeta = DslModelHelper.parseDslNode("/nop/schema/xmeta.xdef", node);

xdef:post-parse

名称类型必须?
xdef:post-parse

xpl

在调用 DslModelParser#parseFromResourceDslModelParser#parseFromNode 时执行该节点内的 Xpl 脚本:

xview.xdef
<view>
<xdef:post-parse xmlns:c="c">
<c:script><![CDATA[
// 可访问变量 _dsl_model 以操作解析后的模型对象
// Note: DSL 根节点变量 _dsl_root 依然可访问
_dsl_model.grids?.forEach(grid=> grid.cols.forEach(col=> col.init()))
_dsl_model.forms?.forEach(form=> form.cells.forEach(cell=> cell.init()))
]]></c:script>
</xdef:post-parse>
</view>

其发生在 DSL 节点合并且已生成模型对象之后。

其调用逻辑见 AbstractDslParser#runPostParse

注意:仅在直接以 xdef:post-parse 所在 *.xdef 生成模型对象时有效,在以 xdef:ref 引入的元模型中无效。

注意:可参考以下代码为 XNode 构造模型对象

// 解析 DSL 获得 XNode 节点
XNode node = XNodeParser.instance().parseFromVirtualPath("/path/to/a.xmeta");
// 根据 DSL 的元模型解析得到模型对象
ObjMetaImpl objMeta = DslModelHelper.parseDslNode("/nop/schema/xmeta.xdef", node);

注意:该脚本的最后一条赋值或 return 语句的结果会成为 DslModelHelper#parseDslNode 的返回结果。

附录

生成节点 class 模型

src/main/java/xx/xx/xx/SchemaCodeGen.java
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);
}
}
precompile2/gen-xxx-xdsl.xgen
<c:script xmlns:c="c"><![CDATA[
codeGenerator.renderModel('/vfs/to/schema/xxx.xdef', '/nop/templates/xdsl', '/', $scope);
]]></c:script>

注册并使用属性类型

首先,注册 Nop 模块初始化器 ICoreInitializer 的实现类,如 XuiCoreInitializer

src/main/resources/META-INF/services/io.nop.core.initialize.ICoreInitializer
io.nop.xui.initialize.XuiCoreInitializer

io.nop.core.initialize.ICoreInitializer 为固定的文件名,其最终由 java.util.ServiceLoader#load 加载,在该文件内可以放置多行 ICoreInitializer 的实现类的全名。

ICoreInitializer 的实现类 XuiCoreInitializer 中调用 StdDomainRegistry#registerStdDomainHandler 以注册自定义属性类型的解析器,如 VueNodeStdDomainHandler

io.nop.xui.initialize.XuiCoreInitializer
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 即可:

io.nop.xui.initialize.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 了:

src/main/resources/_vfs/nop/schema/xui/simple-component.xdef
<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);
});
}
}