EMF(Eclipse Modeling Framework)建模框架能夠幫助我們將模型 (UML, XSD 等 ) 轉 換成為健壯且功能豐富的 Java 代碼。使用 EMF 來搭建應用程序,不但能極大的提高開發效 率,而且還能利用 EMF 提供的很多特性來保證模型的健壯和完善,主要包括模型變化通知機 制,模型的持久化以及模型驗證框架。本文將要介紹的 EMF Validation Framework,是 EMF 的一個重要部分,通過使用 EMF Validation Framework,我們能方便的對定義的 EMF 模型 添加驗證約束,以保證模型數據遵從於用戶自定義的約束。
EMF Validation Framework 提供了對 EMF eObjects 的校驗框架,和 EMF EValidator API 相比
它能提供更復雜全面的驗證並且易用易擴展。EMF Validation Framework 提供了靈活的 驗證定義方式,支持兩種驗證觸發機制:Batch 和 Live。它支持用 JAVA 和 OCL 語言來實 現約束,此外它還支持自定義驗證時的模型掃描算法,並提供了一個 Validation Client Context 來規避不必要的驗證。
EMF Validation Framework 的實現原理
在 EMF Validation Framework 框架中有幾個重要的概念,這些概念構成了驗證框架的基 本部分,我們在下面對其逐一介紹:
約束(constraints)
所有的約束必須實現 IModelConstraint 接口,定義了驗證執行的邏輯(validate()), 並擁有一個約束描述符(一個實現 IConstraintDescriptor 接口的類),該描述符包含了這 個約束的源信息,例如,驗證模式是 live 還是 batch,驗證的目標對象等,圖 1 可以給您 一個基本的概念。
圖 1. 約束
約束的具體實現可以用 JAVA 或者 OCL 語言,用 JAVA 實現的您可以參考下面的圖 2, 其約束都是 AbstractModelConstraint 的子類,它必須實現 validate() 方法。這個方法通 過輸入的驗證上下文(IValidationContext)獲取目標對象、觸發的事件類型等信息,實現 業務驗證邏輯,並將驗證結果通過狀態信息(IStatus)報告給用戶。
圖 2. 約束抽象類和約束上下文等
驗證上下文(Validation Context)
在上面的圖 2 中,我們簡單說明了約束和約束上下文之間的關系,約束上下文記錄了當 前驗證操作的有關信息,包括驗證的目標對象,觸發實時驗證的事件類型,模型變化值等。除了記錄這些上下文信息外,validation context 還提供了一些提高驗證效率的方法。例如 方法 skipCurrentConstraintFor()可以用來指定一些的目標對象是“合格的”可以不執行 某些驗證方法,方法 get/putCurrentConstraintData() 可以用來緩存驗證對象。
驗證模式(Validation Modes)
EMF Validation Framework 提供兩種驗證模式:批量驗證模式(Batch) 和實時驗證模 式(Live)。
批量驗證模式可以對一個集合內的 EObjects 進行校驗 , 通常由用戶的動作觸發,例如 用戶點擊驗證菜單項對整個模型進行驗證。批量驗證模式時,輸入的通常是一個模型元素集 合,輸出的驗證狀態信息(IStatus)包括驗證過程中所發現的所有問題 ,因此這個結果通 常是多狀態的。
批量驗證模式的示例代碼如清單 1 所示:
清單 1. 批量驗證模式的示例代碼
List objects = myResource.getContents();
IValidator validator = ModelValidationService.getInstance ().newValidator(EvaluationMode.BATCH);
IStatus results = validator.validate(objects);
if (!results.isOK()) {
ErrorDialog.openError(null, "Validation", "Validation Failed", results);
}
實時驗證模式則用來實時地對對象內的屬性(值)變更進行校驗。和批量驗證模式不同的 是,它的輸入是屬性(值)變更的通知消息,清單 2 就是實時驗證模式的代碼示例。
清單 2. 實時驗證模式的代碼示例
List notifications = transaction.getChanges();
IValidator validator = ModelValidationService.getInstance().
newValidator(EvaluationMode.LIVE);
IStatus results = validator.validate(notifications);
if (!results.isOK()) {
ErrorDialog.openError(null, "Validation", "Validation Failed", results);
}
約束綁定(Constraint Binding)
定義了這麼多的約束,具體的應用又是如何選擇約束來完成任務呢?約束綁定就是設計來 完成這個任務的 ,通過 org.eclipse.emf.validation.constraintBindings擴展點,應用可 以明確出它擁有的對象,綁定到它所需要的約束。這樣驗證操作就能夠確保對象符合了相應 的約束了。在下面的章節將會詳細介紹如何進行約束綁定。
使用 EMF Validation Framework 來“保護”library 模型
EMF Validation 是一個非常容易使用的框架,本節將通過一個簡單的例子來說明如何使 用 EMF Validation Framework 來保護您的模型。簡單起見,我們使用 Java 編程語言來定 義模型約束。
第一步 准備 Library 模型
為了容易理解,我們就使用 EMF 相關文章中最常見的 Library 樣例作為被保護的模型, 如圖 3 所示,Library 模型很簡單,僅僅包含三個類:Library, Writer, Book,以及一個 BookCategory 枚舉類型。
圖 3. Library 模型
我們在 Eclipse 中創建一個 Java 項目,在"New Java Project"向導中,將工程的名稱 設置為 test.emf.validation,並選擇分離源代碼目錄和輸出目錄。在新建好的 test.emf.validation 項目中建立一個新的 model 目錄,並將 library.ecore 文件保存到 這個目錄中。
為了生成模型的 Java 實現,我們首先需要利用 EMF 提供的向導將 .ecore 模型轉化為 .genmodel 模型。這可以通過如圖 4 所示的"New EMF Generator Model"向導來進行。
圖 4. 使用新建向導生成 Library.genmodel 模型
我們將 Library.genmodel 生成到 model 目錄下,並雙擊其進行編輯。如圖 5 所示,在 .genmodel 的編輯器中,我們選擇 Library 包,並修改其"Base Package"屬性為 emf.model 。這個屬性會影響生成的 Java 代碼的包名稱。
圖 5. 修改 Library 模型的 Base Package 屬性
接下來,我們就可以生成 Library 模型的 Java 實現了。如圖 6 所示,在 Library 包 上單擊右鍵,並在彈出菜單中選擇 Generate Model Code 項,在 test.emf.validation 項 目中生成 Library 的 Java 實現。
圖 6. 生成 Library 模型的 Java 實現
同樣地,選擇 Generate Edit Code 菜單項和 Generate Editor Code 菜單項可以生成 Library 模型的編輯器代碼。在進行完迄今為止的這些步驟之後,我們可以開始編寫代碼來 利用 EMF Validation Framework“保護”Library 模型了。
第二步 定義 batch constraint
前面提到過,用 JAVA 實現的約束都需要擴展 AbstractModelConstrain,實現 validate() 方法。在清單 3 的代碼中,我們定義了一個 constraint 用來驗證圖書館,書 籍,作者三者的名字都不能為空,這個方法通過輸入的驗證上下文(IValidationContext) 獲取目標對象,實現了基本的驗證邏輯,並將驗證結果通過狀態信息(IStatus)報告給用戶 ,使用起來非常直觀。
清單 3. 代碼示例
public class NonEmptyNamesConstraint extends AbstractModelConstraint {
public IStatus validate(IValidationContext ctx) {
EObject eObj = ctx.getTarget();
EMFEventType eType = ctx.getEventType();
// batch validation 時調用
if (eType == EMFEventType.NULL) {
String name = null;
if (eObj instanceof Writer) {
name = ((Writer)eObj).getName();
} else if (eObj instanceof Library) {
name = ((Library)eObj).getName();
} else if (eObj instanceof Book) {
name = ((Book)eObj).getTitle();
}
if (name == null || name.length() == 0) {
return ctx.createFailureStatus(new Object[] {eObj.eClass().getName ()});
}
}
return ctx.createSuccessStatus();
}
}
我們通過 IValidationContext 的 getTarget() 方法來獲取等待驗證的目標模型元素, 並且只有在 eventType 的值為 EMFEventType.NULL 也就是批量驗證模式下才進行名字不為 空的驗證。如果驗證失敗,會返回一個 FailureStatus 對象,驗證成功則返回 SuccessStatus 對象。
第三步 創建 constraint provider extension
定義約束後,我們必須讓框架底層的驗證服務知道它的存在,這是通過創建一個 constraintProviders 擴展點來實現。
如清單 4 所示,在定義一個 constraintProviders 擴展點時,我們首先要指定一個名字 空間,這個名字空間就是我們要驗證的模型,然後我們要指定約束的實現方式,實現類,觸 發模式以及一個唯一的 statusCode。接下來我們要定義在驗證失敗時用戶看到的提示消息 message,message 可以包含任意數目的替代符 {},這些替代符的值可以在驗證失敗時通過 調用 ctx.createFailureStatus(new Object[] {...}) 來進行設置。最後我們需要定義的就 是驗證的目標對象了,這裡我們設定對 Library,Writer,Book 都進行驗證。
清單 4. 定義一個 constraintProviders 擴展點
<extension point="org.eclipse.emf.validation.constraintProviders">
<category
name="Library Constraints"
id="org.eclipse.emf.validation.example.library"/>
<constraintProvider cache="true">
<package
namespaceUri="platform:/resource/test.emf.validation/model/Library.ecore"/>
<constraints categories="emf.validation.example.library">
<constraint
lang="Java"
class="emf.model.library.validation.constraints.NonEmptyNamesConstraint"
severity="ERROR"
mode="Batch"
name="Non-Empty Names"
id="emf.validation.example.library.NameNotEmpty"
statusCode="1">
<description>
All items in a library model should have some unique identifier or name.
</description>
<message>
The required feature ‘ name ’ of ‘ {} ’ must be set.
</message>
<target class="Library"/>
<target class="Writer"/>
<target class="Book"/>
</constraint>
</constraints>
</constraintProvider>
</extension>
第四步 將定義好的 Constraint 綁定到應用程序
到目前為止,我們已經定義了一個約束並且將它注冊到驗證服務。接下來我們需要把這個 約束綁定到應用程序,讓應用程序知道它的存在。清單 5 介紹了如何通過定義一個 constraintBindings 的擴展點來實現。
清單 5. 定義一個 constraintBindings 的擴展點
<extension
point="org.eclipse.emf.validation.constraintBindings">
<clientContext
default="false"
id="emf.validation.example.libraryContext">
<selector class="emf.model.library.validation.constraints.ValidationDelegateClientSelector" />
</clientContext>
<binding
context="emf.validation.example.libraryContext"
category="emf.validation.example.library"/>
</extension>
可以看到,在進行約束綁定時我們首先要定義一個客戶端上下文(client context),這 通過定義一個對象選擇器(IClientSelector)來限定上下文所包含的模型對象集合。接下來 再定義用來檢查這些模型對象數據的約束,也就是我們在前面所定義的用來檢查圖書館對象 名稱是否為空的約束。
第五步 通過 validation service 來調用 batch constraint
完成上面這些步驟後,我們可以通過驗證服務來調用約束對模型進行驗證,見清單 6 的 代碼。
清單 6. 調用約束對模型進行驗證
IBatchValidator validator = (IBatchValidator) ModelValidationService.getInstance()
.newValidator(EvaluationMode.BATCH);
validator.setIncludeLiveConstraints(true);
IStatus status = validator.validate(selectedEObjects);
第六步 運行前面創建的 constraint
在 Eclipse 中點擊 Run->Run.., 在彈出的窗口中創建並運行一個 Eclipse Application。在新的工作平台中新建一個項目,然後通過如圖 7 所示的“New Library Model”向導創建一個圖書館模型。
圖 7. 創建模型項目
為了驗證之前定義的約束是否生效,創建如圖 8 所示的圖書館模型,可以看到該模型只 定義了一個沒有名稱的圖書館對象:
圖 8. 圖書館模型
如圖 9 所示,選中 library 對象,點擊右鍵,選擇 validate 菜單項。
圖 9. validate 操作
如圖 10 所示,一個驗證失敗的警告框彈出,點擊 Details,可以看到驗證失敗的原因是 圖書館的名字屬性沒有設置。
圖 10. 驗證結構提示
第七步 把 batch constraint 轉變成 live constraint
我們可以把之前定義的 batch constraint 修改成 live constraint,這樣當模型指定的 屬性(值)發生變化時驗證服務會收到相應的通知從而觸發驗證。通知消息包括了諸如發生 改變的屬性及該屬性變化前後的值等信息。在擴展點定義中去聲明那些需要在取值發生變化 時進行實時驗證的屬性。
清單 7 是對之前定義的 bacth constraint 定義的修改,當事件類型不為空是,也就是 說驗證服務收到了模型屬性變更的通知事件,此時觸發驗證檢查新的屬性值是否為空。
清單 7. 之前定義的 bacth constraint 定義的修改
public class NonEmptyNamesConstraint extends AbstractModelConstraint {
public IStatus validate(IValidationContext ctx) {
EObject eObj = ctx.getTarget();
EMFEventType eType = ctx.getEventType();
// bacth validation 時調用
if (eType == EMFEventType.NULL) {
String name = null;
if (eObj instanceof Writer) {
name = ((Writer)eObj).getName();
} else if (eObj instanceof Library) {
name = ((Library)eObj).getName();
} else if (eObj instanceof Book) {
name = ((Book)eObj).getTitle();
}
if (name == null || name.length() == 0) {
return ctx.createFailureStatus(new Object[] {eObj.eClass().getName ()});
}
// live validation 時調用
} else {
Object newValue = ctx.getFeatureNewValue();
if (newValue == null || ((String)newValue).length() == 0) {
return ctx.createFailureStatus(new Object[] {eObj.eClass().getName ()});
}
}
return ctx.createSuccessStatus();
}
}
下面清單 8 的代碼是對 constraintProviders 擴展點的修改,可以看到首先驗證模式由 batch 變成了 live,此外我們定義了感興趣的驗證目標 EClass, 目標屬性 EStructuralFeature 和觸發事件類型。
清單 8. constraintProviders 擴展點的修改
<extension
point="org.eclipse.emf.validation.constraintProviders">
<category
name="Library Constraints"
id=" org.eclipse.emf.validation.example.library "/>
<constraintProvider cache="true">
<package namespaceUri="platform:/resource/test.emf.validation/model/Library.ecore"/>
<constraints categories=" emf.validation.example.library ">
<constraint
lang="Java"
class=" emf.model.library.validation.constraints.NonEmptyNamesConstraint "
severity="ERROR"
mode="Live"
name="Non-Empty Names"
id=" emf.validation.example.library.NameNotEmpty "
statusCode="1">
<description>
All items in a library model should have some unique identifier or name.
</description>
<message>
A {0} has been found to have no unique identifier (name or title).
</message>
<target class="Library">
<event name="Set">
<feature name="name"/>
</event>
<event name="Unset">
<feature name="name"/>
</event>
</target>
<target class="Writer">
<event name="Set">
<feature name="name"/>
</event>
<event name="Unset">
<feature name="name"/>
</event>
</target>
<target class="Book">
<event name="Set">
<feature name="title"/>
</event> <event name="Unset">
<feature name="title"/>
</event>
</target>
</constraint>
</constraints>
</constraintProvider>
</extension>
第八步 調用 live constraint
live constraint 的調用方式和之前介紹的 batch constraint 的調用方式有所 不同,請參考清單 9 中的代碼。我們知道 live validation 在收到注冊目標對象屬性 (值)變更的通知消息時觸發,因此在調用 live constraint 之前需要先定義一個 EContentAdapter,然後這個 EContentAdapter 綁定到目標資源,這樣目標資源發生屬性 (值)變更時所發出的通知消息就能被我們的constraint 獲知。
清單 9. live constraint 的調用方式
Resource r = (Resource)i.next();
if (!resourceHasAdapter(r)) {
EContentAdapter liveValidationContentAdapter =
new LiveValidationContentAdapter();
r.eAdapters().add(liveValidationContentAdapter);
}
總結
通過一個簡單的例子您已經基本了解了使用 EMF Validation Framework 進行模型驗證的 基本過程,可以看到通過使用 EMF Validation Framework 我們能方便地對 EMF 模型添加驗 證約束來保證模型數據遵從於用戶自定義的規則。