现在的位置: 首页 > 综合 > 正文

EMF学习笔记

2013年05月03日 ⁄ 综合 ⁄ 共 17376字 ⁄ 字号 评论关闭

 

模型实体类

 

EMF是基于两种元模型来构建的,分别是Ecore和Genmodel。

 

 

Ecore:主要用于定义模型结构的相关信息,并对外提供了一个根对象(犹如XML文件的根节点)用来代表要显示的模型,该对象的子节点为package(包),package的子节点为class(类),class的子节点为attribute(类的属性字段),这样整个树形结构就代表了所要表示的EMF模型结构。

Genmodel:主要包含有关代码生成的相关信息(如:文件生成路径及文件名),和一些控制参数用来控制代码怎样生成。

 

EMF采用模型驱动开发方法,在无需编码的情况下,我们可以基于模型使用EMF为我们生成以下几样东西。
1,与模型相对应的EMF实体类。
2,基于实体类的各种ItemProvider模型适配器类,通过该适配器类可以很好的将EMF模型绑定到Jface组件中去,稍后具体介绍。
3,editor-plugin项目,可理解为基于EMF应用的一个插件项目。
4,测试bundle。

 

 

这里3和4不做介绍,主要讲解1,2。然后自定义一个Jface组件,将EMF模型应用与该组件之上。

 

在开始之前,我们需要了解以下几个名词的含义,也是文中多处提到的几个词:
1实体类:就像Hibernate ORM为我们生成的实体类一样,EMF也会为我们生成这样的实体类,并且这些实体类含有更高级的特性(如:类属性状态发生变化时,实体类具有通知功能)

2适配器类:EMF的模型结构可能非常复杂(如可能含有多个实体类,并且类结构各不相同),这样当视图组件直接使用这些实体类的时候就变得非常困难(要判断出是哪一个实体类,针对该实体类怎样显示视图等等..),因此EMF针对这些实体类生成了一些适配器类,通过实现指定的接口,让适配器类来设计实体类的显示,而视图只需要选择合适的适配类便可。

3适配器工厂:上面讲到视图通过适配器类避免了与实体类的直接交互,然而视图又是如何得到合适的适配器类的呢?那便是通过适配器工厂类。

 

模型定义如下:

UML模型定义
从模型定义来看Library便是Ecore所对外提供的用来表示模型的根对象。

首先来看EMF为我们生成的模型实体类(假设这些实体类分布在org.emf.demo这个bundle之中)
EMF会为我们生成3个package,分别是:
org.emf.demo.library:包含实体类的接口;用于创建实体类的工厂(libraryFactory);访问模型元数据的实用类(LibraryPackage)

org.emf.demo.library.impl:实体类接口的实现类;实体类工厂的实现类(LibraryFactryImpl);LibraryPackage的实现类(LibraryPackageImpl)

org.emf.demo.library.util:针对该模型的适配器工厂类(LibraryAdapterFactory)
注:libraryFactory和LibraryPackage都是单例模式的,可通过其成员变量eINSTANCE来得到对象的实例。

 

 

在EMF中,所有的实体类接口全部继承EObject,换个角度,同Java领域的Object对象一样,EObject是EMF中所有类的基类。在实体类的定义中,我们强调过,除了具有一般JavaBean的特性之外,EMF实体类具有更高级的特性,因此其代码实现也较普通的javaBean更加复杂。

在EMF所生成的实体类的set方法中,除了要对成员变量进行赋值外,还要对其适配器类进行变更通知操作。以Book实体类的setPages方法为例:

  1. public void setPages(int newPages){  
  2.  int oldPages = pages;  
  3.  pages = newPages;  
  4.  if (eNotificationRequired())  
  5.   eNotify(new ENotificationImpl(this, Notification.SET, ..., oldPages, pages));  
  6.      }  
  7. }  

eNotificationRequired()方法:判断该实体类是否含有适配器类。
eNotify()方法:对适配器类进行变动通知操作;
至于实体类状态发生变化时为什么要通知其适配器类,在以后的篇幅中会有介绍。

在UML模型中,实体类之间的关系是多种多样的,单向引用,双向引用,聚合引用等等,为了表示这种关系的不同,EMF为我们生成的实体类代码也是不一样的。

 

单向引用(one-way reference):
在单向引用中,EMF所生成的实体类set方法和无引用情况是相同的(成员变量赋值、适配器类通知)
只是get方法的实现不同,以Book实体类的getWriter()方法为例:

  1. public Writer getWriter() {  
  2.  if (Writer != null && Writer.eIsProxy()) {  
  3.   InternalEObject oldWriter = (InternalEObject)Writer;  
  4.   Writer = (Writer)eResolveProxy(oldWriter);  
  5.   if (Writer != oldWriter) {  
  6.    if (eNotificationRequired())  
  7.     eNotify(new ENotificationImpl(this, Notification.RESOLVE, WebpagePackage.BOOK__Writer, oldWriter, Writer));  
  8.   }  
  9.  }  
  10.  return Writer;  
  11. }  

当Writer和Book两个实体类分别存储与不同的资源文件中时,EMF是采用延迟加载模式来加载对象的,因此这里的Writer对象可能只是Writer的一个代理类,而不是真正的对象引用,所以在getWriter()方法里,首先要判断其是不是代理类(eIsProxy()方法),如果是则调用eResoleProxy()方法来加载目标对象,然后判断目标对象与代理对象是否相同(引用同一个对象),如果不同则Book的Writer属性状态便发生了变化,要对其适配器类进行通知。

 

双向引用(two-way reference):
同单向引用相比,get方法没有变化,set方法的实现不同。以Book对象的setWriter()方法为例:

  1. public void setWriter(Writer newWriter){  
  2.  if (newWriter != Writer){  
  3.   NotificationChain msgs = null;  
  4.   if (Writer != null)  
  5.    msgs = ((InternalEObject)Writer).eInverseRemove(this, ..., msgs);  
  6.   if (newWriter != null)  
  7.    msgs = ((InternalEObject)newWriter).eInverseAdd(this, ..., msgs);  
  8.   msgs = basicSetWriter(newWriter, msgs);  
  9.   if (msgs != null) msgs.dispatch();  
  10.      }else if (eNotificationRequired())  
  11.   eNotify(new ENotificationImpl(this, ...));  
  12.    }  
  13. }  
  14.   
  15. public NotificationChain basicSetWriter(Writer newWriter, NotificationChain msgs){  
  16.  Writer oldWriter = Writer;  
  17.  Writer = newWriter;  
  18.  if (eNotificationRequired()){  
  19.         ENotificationImpl notification = new ENotificationImpl(this, ...);  
  20.   if (msgs == null)   
  21.    msgs = notification;   
  22.   else   
  23.    msgs.add(notification);  
  24.      }  
  25.  return msgs;  
  26. }  

注:Book与Writer互为引用
从Book角度看,Book是引用端,Writer是被引用端;
相反从Writer的角度看,Writer是引用端,而Book是被引用端(^_^)
在setWriter()方法中,除了执行引用端的basicSetWriter()方法外,还执行了被引用端(Writer端)的eInverseRemove和eInverseAdd方法,这样通过Book端的操作,实际上是影像了两端的结果。

我们可以编写代码来进行测试:

  1. LibraryFactory factory=LibraryFactory.eINSTANCE;//得到创建实体类的工厂类
      
  2. Book book=factory.createBook();//创建Book对象
      
  3. Writer writer=factory.createWriter();//创建Writer对象
      
  4. writer.setName("zhangsan");  
  5. book.setTitle("emf demo");  
  6. book.setPage(123);  
  7. book.setWriter(writer);//只是在book端执行了set操作
      
  8.   
  9. List<Book> books=writer.getBooks();//在writer端依然能够遍历到book
      
  10. for(Book b:books){  
  11.  System.out.println(b.getTitle());  
  12. }  

虽然我们只是在book端执行了setWriter方法,而没有在writer端执行添加book的方法,但是在writer端依然能够遍历到Book对象。

聚合引用:
所谓聚合引用,就是一个对象是另一个对象的容器;比如上面的Library和Book,Library便是Book的容器。
在EMF中,容器和它所包含的对象存储在同一资源文件中,因此在加载的时候不需要使用代理机制。
在聚合引用中,每个对象只能有一个容器,因此在将一个对象加入到某个容器之前,要将该对象从现在的容器中移除出去。为了实现该功能,EObjectImpl类中设计了一个成员变量eContainer用来表示实体类对象的容器,这样我们就可以通过book.eContainer()方法来得到Library对象。

 

注:EMF所生成的变量或方法都会含有@generated标注,当执行regenerate操作时EMF只会覆盖含有@generated标注的方法,而自己手工添加的方法不会被覆盖。

 

 

 

模型适配器类

适配器类是通过EMF.Edit这个框架生成的,通过它生成的适配器类为EMF模型起到了编辑器的作用,包括:
1为Jface组件(如TableViewer、TreeViewer等)提供ContentProvider和LabelProvider
2提供常用命令操作(添加、删除、更新、undo、redo等)。

在Jface组件中,视图内容是通过ContentProvider接口来提供的,而标签和图标的显示是通过LabelProvider接口来实现的,
在EMF中,AdapterFactoryContentProvider间接实现了IContentProvider接口,而AdapterFactoryLabelProvider间接实现了ILabelProvider接口,因此,Jface组件便可以通过他们来获取组件的内容和标签的显示信息。
上一篇文章已经提到,视图不直接与EMF实体类交互,而是通过其适配器类的处理来完成EMF模型到视图的绑定过程。
这些适配器在EMF中都是以ItemProvider的形式存在的(如实体类Book的适配器类便是BookItemProvider类),并且由统一的工厂类来对其进行管理(LibraryItemProviderAdapterFactory),因此在构造AdapterFactoryContentProvider对象时,只要将该适配器工厂类以
构造参数的形式传递进去,AdapterFactoryContentProvider便可以通过适配器工厂将代码逻辑转交给相应的适配器类。

可通过一段代码来看一下适配器类的使用:
假设我们要将EMF模型应用到TreeViewer组建上,则需要文本提供器实现ITreeContentProvider接口,AdapterFactoryContentProvider实现了这个接口,当我们调用getChildren()获取子节点时,它会进行如下处理:

  1. public Object [] getChildren(Object object)  
  2. {  
  3.     //调用适配器工厂类来获取object的适配器类
      
  4.     ITreeItemContentProvider treeItemContentProvider =   
  5.       (ITreeItemContentProvider)adapterFactory.adapt(object, ITreeItemContentProviderClass);  
  6.     return  
  7.       (treeItemContentProvider != null ?  
  8.         treeItemContentProvider.getChildren(object) ://执行适配器类的getChildren()方法
      
  9.         Collections.EMPTY_LIST).toArray();  
  10. }  

该方法首先会调用适配器工厂类的adapt方法来获取指定object的适配器类,
然后再调用适配器类的getChildren()方法,这样便将代码逻辑转交给适配器类去处理。

整个处理过程是这样的,但是这两个步骤我们也应该明确其实现细节。
1适配器工厂类(adapterFactory)如何通过指定object来获取其适配器类
我们可以看一下adapt()方法的实现(注:org.eclipse.emf.common.notify.impl.AdapterFactoryImpl类中声明)

  1. public Object adapt(Object target, Object type){  
  2.     if (target instanceof Notifier){//判断target对象是不是emf实体类
      
  3.       return adapt((Notifier)target, type);//如果是返回该实体类的适配器类
      
  4.     }  
  5.     else{  
  6.       return target;//如果不是emf实体类则直接返回该对象
      
  7.     }  
  8. }  

方法首先会判断传入的target对象是不是emf实体类
(注:所有的emf实体类接口全部继承至EObject,而EObject接口又继承至Notifier,所以我们可以通过target instanceof Notifier来判断target是不是emf实体对象)
如果是emf实体,则通过adapterFactry返回其适配器类;
而如果不是emf实体,则直接返回了该对象。
代码这样设计其实是有以下好处的:
在使用EMF模型作为视图组件的提供器之后,我们可能还想将其他模型也整合到其中;换个角度说,视图的提供器可能是EMF模型和其他模型的混合体。那么adapt方法的处理逻辑便能满足这种需求。
比如,我们可以自己编写一个与EMF模型无关的类,让它实现ITreeItemContentProvider接口,当调用adapterFactory的adapt方法时,因为它不是EMF实体类,便会直接返回该对象,然后便可以将具体的业务操作转接给该对象进行处理。
该方法也从另一面说明了在EMF中,所有的ItemProvider都是适配器类,但并不是所有的适配器类都是ItemProvider。比如这里我们创建的对象,它也是适配器类,但不是作为ItemProvider的形式出现的。

2适配器类如何处理接下来的逻辑
如果该适配器类是EMF生成的(即以ItemPrvider形式出现的),那么它会继承org.eclipse.emf.edit.provider.ItemProviderAdapter这个模板类,然后调用模板类的相应方法去处理。如果该适配器类是我们手工生成的,则需要我们自己去编写方法的实现。

有了这些适配器类之后,构建Jface组件将会变得很容易,我们还没有手工编写任何代码,便具备了几乎所有构建组件的基本条件。
构建TreeViewer组件如下:

  1. TreeViewer treeViewer=new TreeViewer(container);  
  2. AdapterFactory adapterFactory=new LibraryItemProviderAdapterFactory();//创建适配器工厂类
      
  3. treeViewer.setContentProvider(new AdapterFactoryContentProvider(adapterFactory));//指定内容提供器
      
  4. treeViewer.setLabelProvider(new AdapterFactoryLabelProvider(adapterFactory));//指定标签提供其
      
  5. //创建input数据源
      
  6. LibraryFactory librayFactory=LibraryFactory.eINSTANCE;  
  7. Library library=librayFactory.createLibrary();  
  8. library.setName("国家图书馆");  
  9. Book book=librayFactory.createBook();  
  10. book.setTitle("emf head first");  
  11. book.setCategory(Category.CATEGORY1);  
  12. Writer writer=librayFactory.createWriter();  
  13. writer.setName("张三");  
  14. book.setWriter(writer);  
  15. library.getBooks().add(book);  
  16. library.getWriters().add(writer);  
  17. treeViewer.setInput(library);  

可以看到,我们将主要操作都放在了数据源input值的构造上,而其他操作通过构建EMF适配器工厂来进行处理。

构建TableViewer组件:
EMF为我们生成的模型并不直接支持TableViewer组件,需要稍加修改。
首先在适配器工厂类的构造函数中添加新类型支持。
如:在LibraryItemProviderAdapterFactory中添加对ITableItemLabelProvider类型的支持。

  1. public LibraryItemProviderAdapterFactory() {  
  2.  supportedTypes.add(IEditingDomainItemProvider.class);  
  3.  supportedTypes.add(IStructuredItemContentProvider.class);  
  4.  supportedTypes.add(ITreeItemContentProvider.class);  
  5.  supportedTypes.add(IItemLabelProvider.class);  
  6.  supportedTypes.add(IItemPropertySource.class);  
  7.  supportedTypes.add(ITableItemLabelProvider.class);//新增table的
      
  8. }  

然后在每个适配器类所实现的接口中添加ITableItemLabelProvider接口
如:public class BookItemProvider
 extends ItemProviderAdapter
 implements
  IEditingDomainItemProvider,
  IStructuredItemContentProvider,
  ITreeItemContentProvider,
  IItemLabelProvider,
  IItemPropertySource,
  ITableItemLabelProvider//添加此接口

最后编写并覆盖getColumnText()方法和getColumnImage()方法。

接下来的构建步骤与TreeViewer相同。

 

 

 

 

EMF命令

在EMF中可以通过EditingDomain接口来编辑模型。
AdapterFactoryEditingDomain实现了EditingDomain接口,和AdaperFactoryContentProvider一样,将操作逻辑通过适配器工厂转接给适配器类进行处理。通过AdapterFacotryEditingDomain还可以访问CommandStack对象,所有对模型结构的修改命令都将存储在这里。

更改模型结构的方法:
假设两个EMF实体类Companny和Department属于1对多关系,现在公司想删除指定部门,可通过以下几个方法来完成操作:

1.不使用命令的普通方式

  1. Department d=...  
  2. Company c=...  
  3. c.getDepartments().remove(d);  

因为所有的Emf实体都间接的实现了Notifier接口,因此在执行remove操作时,EMF的通知机制可以将模型结构的变动及时反映到视图组件的刷新.(稍后会详写这里)

2.使用org.eclipse.emf.edit.command.RemoveCommand命令来执行删除操作

  1. Department d=...  
  2. Company c=...  
  3. EditingDomain ed=...  
  4. RemoveCommand cmd=new RemoveCommand(ed,c,CompanyPackage.eINSTANCE.getCompany_Departments(),d);  
  5. ed.getCommandStack().execute(cmd);  

通过命令的方式执行虽然代码逻辑较多,但有以下好处:
1.对已执行出的命令执行撤销操作(undo,redo)
2.可判断模型结构是否被修改(用来触发save操作)
3.可判断一个命令是否可执行

直接new一个命令出来,这个命令是有针对性的(如:这里的命令只针对company实体),因此可重用性比较低,我们可以通过EditingDomain接口的createCommand()方法来创建命令,来使这个命令针对所有模型实体通用。
createCommand(Class commandClass, CommandParameter commandParameter)这个方法有两个参数:
commandClass:为具体的命令类,如org.eclipse.emf.edit.command.RemoveCommand
commandParameter:为命令参数,通过此种方式,可编写如下代码:

  1. Department d=...  
  2. EditingDomain ed=...  
  3. Command cmd=RemoveCommand.create(ed,d);  
  4. ed.getCommandStack().execute(cmd);  

注:这里使用了RemoveCommand的静态方法create,而不是new一个RemoveCommand对象,
而且传递的参数也只有要删除的对象(department),而不像之前还要包括所删除对象的父对象(company),那么该父对象是怎样得到的呢?

create静态方法如下:

  1. public static Command create(EditingDomain domain, Object value)   
  2.   {  
  3.     return domain.createCommand(  
  4.       RemoveCommand.class,  
  5.       new CommandParameter(nullnull, Collections.singleton(value)));  
  6.   }  

可以看到,方法调用了EditingDomain的createCommand方法,然后AdapterFactoryEdintingDomain(EdintingDomain的实现类)
将命令的创建转接给适配器类,让适配器类处理命令的创建过程。

createCommand方法大致如下:

  1. public Command createCommand(Class commandClass, CommandParameter commandParameter)  
  2.   {  
  3.     Object owner = ...  // get the owner object for the command
      
  4.     IEditingDomainItemProvider adapter =   
  5.       (IEditingDomainItemProvider)  
  6.         adapterFactory.adapt(owner, IEditingDomainItemProvider.class);  
  7.     return adapter.createCommand(owner, this, commandClass, commandParameter);  
  8.   }  

真的代码逻辑复杂的多,方法中省略了owner对象的获取步骤,然而owner对象是如何得到的呢?
从上面的create方法来看,在构造CommandParamter对象时,将要删除的对象以构造参数的形式传递过去(Collections.singleton(value)),
然后再将commandParameter作为参数传递到createCommand方法中,这样在createCommand方法中我们便可得到要删除的value对象,
而通过value对象的eContainer()方法,便可以获取对象的容器,即父对象owner。
方法随后调用了adapterFactory的adapt方法,来获取该父对象的适配器类,最后将业务转交给该适配器类进行处理。

适配器类会根据commandClass的值来选择与之相对应的命令执行构建操作。

 

 

 

Query查询

EMF查询框架主要由 org.eclipse.emf.query 这个plugin提供,创建并执行一个EMF查询,大概分为如下几步:
1,创建查询数据源;
2,构造EObjectCondition查询条件;
3,使用EMF查询框架提供的SELECT对象和UPDATE对象编写查询和更新语句;
4,执行语句得到查询或更新结果;
5,对执行结果进行检测,确保执行过程中没有发生异常。

EMF提供了两套Query对象,SELECT和UPDATE,分别用于模型的查询操作和更新操作。
SELECT对象的类图关系如下:

SELET对象结构图

类似SQL语句的from和where从句,SELECT对象的构建需要FROM和WHERE两个对象,分别代表查询范围和查询约束条件(EObjectCondition)。
除了这两个对象之外,SELECT对象还可能包含下参数信息:
maximumResultSize:设置查询的最大返回值;
cancellable:查询是否可撤销,默认值为true;
progressMonitor:查询监听器。

构建EMF SELECT查询代码大致如下:

  1. IQueryResult result = new SELECT(//查询结果封装成IQueryResult对象
      
  2.         new FROM(searchScope),  
  3.         new WHERE(conditions))  
  4.     .execute();  
  5. //检测执行过程是否出现异常
      
  6. if (result.getException() != null) {  
  7.     log(result.getException());  
  8. else {  
  9.     for (Object next : result) {//打印查询结果
      
  10.         System.out.println("Found " + ((Book) next).getTitle());  
  11.     }  
  12. }     

其中FROM对象的构造参数可以是单一的EObject对象,也可以是Collection集合,还可能是IEObjectSource对象实例。
较为复杂的是WHERE对象的构建,或者说是其构造参数EObjectCondition约束条件的构建。

EMF查询框架预定义很多查询条件类继承至EObjectCondition,然而熟练的使用这些类并不容易,
还有一种方法是通过OCL约束表达式构造 BooleanOCLCondition 对象,如果熟练OCL语法,用此种方式相对比较容易。
但也有不足,就是OCL的解析速度相对较慢。

  1. EObjectCondition tolerableRead = new BooleanOCLCondition<EClassifier, EClass, EObject>(  
  2.     ocl.getEnvironment(),//ocl根环境
      
  3.     "self.category <> BookCategory::Mystery implies self.pages < 200",//ocl约束
      
  4.     EXTLibraryPackage.Literals.BOOK);//约束上下文  

构造参数中,还可以不指定约束的上下文(即第3个构造参数为null),这样任何EMF实体如果含有category属性都会在该实体类上执行
"self.category <> BookCategory::Mystery implies self.pages < 200"约束检测。

当查询条件比较多的时候,可以通过EMF的核心查询框架将多个查询条件进行组合:

抱歉!评论已关闭.