启用EMF的自动生成UUID功能

缺省条件下,EMF不会为每个对象维护一个唯一的id,这在一些情况下不太方便,例如要在web环境下编辑一个EMF模型实例,通常需要在URL里传递对象的id以便确定目标。

有两种方式解决这类问题。第一种是在模型里给每个对象增加一个id属性,并维护使其保持唯一,例如在XXXFactorycreateXXX()方法里为这个属性赋值。

另一种方式是借助EMF的功能。其实EMF的XMIResource维护了一个id与对象的映射表,并且会为每个新创建的对象自动赋值,只要稍微设置一下就可以启用这个功能,具体的方法如下。

  1. xxx.genmodel里选中package节点,在属性视图里把Resource Type属性的值设置为XMI

file

  1. 重新生成代码,EMF生成的代码里会比原来多出XXXResourceImpl.java文件。

  2. XXXResourceImpl.java里,覆盖useUUIDs()方法,让其返回true(缺省返回的是false)。

  3. 重新生成代码。(删除以前的Manifest.MFplugin.xml文件以便这两个文件能得到更新)

这样,如果用文本方式查看新创建的模型实例文件,会发现每个对象都多出一个xmi:id属性,如xmi:id="_gitpslkoEd2PdI7FPnuunA"。要在程序里使用这个id,只需要用resource.getId(eobject)即可得到,其中resource可以通过eobject.eResource()很方便的获得。

参考链接

How To Enable UUID In EMF Generated Model To Get Copy&Paste Working
这个链接里的方法太麻烦了,但功效一样。

Eclipse GMF - Enabling UUIDs in Semantic model - The Simplest Way
GMF的时候这个链接里的方法应该比较方便

解决EMF里引用对象的刷新问题

假设在ecore模型里定义了两个类:产品(Product)和制造商(Manufacturer),Product通过名为manufacturer的多对一引用与Manufacturer建立关系。在应用程序里,有一些表格(TableViewer)需要在显示产品信息的同时,还要显示制造商相关信息,如制造商名称。缺省条件下,因为这些表格里每一行是一个Product实例,表格的文字更新由AdapterFactoryLabelProvider通过ProductItemProvider实现,所以在制造商名称被改变的时候产品表格无法得到更新。

当然可以直接调用viewer.refresh()方法刷新表格,但这要求具有对viewer的引用,况且我们不知道以后还会有多少个这样的viewer需要刷新。更好的解决办法是修改ProductItemProvider,让它维护一个Adapter(即EMF里的模型监听器),并把这个监听器注册到Product对应的Manufacturer实例上。监听器的注册可以在getText()方法里实现(也许有更合适的地方),别忘了在dispose()方法里要删除这个监听器。此外,要在.genmodel里把Product的Provider Type属性值从缺省的Singleton改为Stateful,如下图,这样每个Product都对应一个ProductItemProvider实例,从而对应一个这样的监听器。

file

以下是ProductItemProvider里部分相关代码:

//Define an adapter
    protected Adapter manufacturerAdapter =
         new AdapterImpl()
         {
           public void notifyChanged(Notification notification)
           {
             //notify product viewers if manufacturer's name changed
             if (notification.getFeatureID(Manufacturer.class) == ProductPackage.MANUFACTURER__NAME)
               fireNotifyChanged(new ViewerNotification(notification, ProductItemProvider.this.getTarget(), false, true));
           }
         };

    public String getText(Object object) {
        Product product = (Product) object;

        //Add following codes to maintain the adapter
        Manufacturer manufacturer = product.getManufacturer();
        if (manufacturer != manufacturerAdapter.getTarget()) {
            if (manufacturerAdapter.getTarget() != null)
                manufacturerAdapter.getTarget().eAdapters().remove(manufacturerAdapter);
            if (manufacturer != null)
                manufacturer.eAdapters().add(manufacturerAdapter);
        }

        String label = var.getName();
        return label;
    }

  //Remove adapter when ProductItemProvider disposes
    public void dispose() {
        if (manufacturerAdapter.getTarget() != null)
            manufacturerAdapter.getTarget().eAdapters().remove(manufacturerAdapter);
        super.dispose();
    } 

最后,需要覆盖ProductItemProvider的notifyChanged()方法,在switch里增加如下代码:

case ProductPackage.Product__Manufacturer:
    fireNotifyChanged(new ViewerNotification(notification, notification.getNotifier(), false, true));
    return;

参考链接

http://dev.eclipse.org/newslists/news.eclipse.tools.emf/msg18730.html

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2007/09/03/880854.html

Eclipse 3.3里新TreeViewer给EMF应用程序带来的一个问题

以前在Eclipse 3.2里做的一个EMF应用程序,到3.3里发现一些TreeViewer里标签显示的格式不对,例如原来显示“Condition true”的,在3.3下可能显示“1”。调试了一下发现这些TreeViewer在为每个节点获得标签时,若相应的XXXItemProvider实现了ITableItemLableProvider(即应用程序里有TableViewer也用到这个XXXItemProvider)时,会调用getColumnText()而不是getText()来得到文本内容。

在Eclipse新闻组里搜到这个帖子讲的是同一件事(新闻组是遇到问题后第一反应),原因不在EMF,是Eclipse 3.3里对TreeViewer的实现有了变化,新的实现把原来的TreeViewer当成只有一列的特殊的TableViewer来对待,以致EMF也把TreeViewer当成了TableViewer,当然会去找getColumnText()了。解决的办法也不复杂,新闻组里那个帖子也提到了,我给帖到这方便大家参考吧。

/**
 * @Added
 * Solve a problem raised in jface 3.3 tree viewer
 * @see http://dev.eclipse.org/newslists/news.eclipse.tools.emf/msg25409.html
 *
 */
class WorkaroundAdapterFactoryLabelProvider extends AdapterFactoryLabelProvider {
    /**
    * @param adapterFactory
    */
    public WorkaroundAdapterFactoryLabelProvider(AdapterFactory adapterFactory) {
        super(adapterFactory);
    }

    @Override
    public Image getColumnImage(Object object, int columnIndex) {
        return super.getImage(object);
    }

    @Override
    public String getColumnText(Object object, int columnIndex) {
        return super.getText(object);
    }
}

有了上面这个类,然后把原来setLabelProvider()里的AdapterFactoryLabelProvider换成它就可以了。EMF以后的版本应该会解决这个问题。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2007/08/21/864579.html

脱离Eclipse环境使用EMF

一旦熟练使用EMF后,有新项目我总是习惯于先使用工具构造出数据模型,然后让EMF帮我生成java代码。当模型需要修改时,也是用工具修改模型本身,然后让EMF把改动更新到java代码,从而保证模型与代码的同步。最近的一个基于struts的Web项目里我试验了脱离Eclipse运行环境使用EMF,发现比想象中的要更容易,以下是一些经验总结。

  1. EMF可以脱离Eclipse环境使用,所以不论你要做的项目是纯swt应用程序、swing应用程序、基于web的应用程序甚至没有GUI的应用程序,这些非Eclipse插件的项目也都可以利用EMF生成的模型代码。不过EMF生成的.edit和.editor部分就难以利用了,因为.edit主要为jface的各种viewer提供支持,.editor则是基于Eclipse的一个编辑器。

  2. 要脱离Eclipse独立使用EMF生成的代码,需要在项目里包含EMF的运行库,可以在EMF下载页面找到这个名为Standalone的下载项,下载以后要让里面的.jar文件都包含在项目classpath里。

  3. 除添加这些库文件以外,还有少量初始化工作需要在你的应用程序启动时执行。在EMF-FAQ页面专门介绍了初始化的方法,基本上要做的就是在程序启动时执行下面两条命令:

XXXPackage xxxPackage = XXXPackage.eINSTANCE;
Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().put("xxx", new XMIResourceFactoryImpl());

Update(2009-2-19): 如果模型文件包含了除emf以外的名称空间声明(例如要打开一个.emx文件),则需要对它们进行注册,下面的代码注册了gmf1.0的名称空间:

((Delegator)EPackage.Registry.INSTANCE).put("http://www.eclipse.org/gmf/runtime/1.0.0/notation", NotationPackage.eINSTANCE);
  1. 用EMF生成模型代码的步骤和平时基本没有区别,仍然是".ecore->.genmodel->java代码"。一般来说,直接在这个EMF项目里以EMF生成的java代码为基础继续开发就可以了,如果你用其他IDE则可以把这些java代码复制过去使用,只是注意一下每次修改ecore后要重复这个生成和复制代码的过程。

  2. 关于多个模型和跨模型引用的处理。EMF的一个好处是可以把整个项目的模型保存为多个文件,并且允许文件之间的相互依赖。例如一个在线商店系统里,在设计的时候可以把模型分为用户信息、商品信息和订单信息三大部分,显然订单信息依赖用户和商品信息,所以对用户和商品的管理可以独立进行开发,一方面提高了模块化程度,也增加了重用的可能性,例如在另外一个项目里可以只做很小的修改甚至不做修改的重用用户管理部分或是商品管理部分。

EMF是通过ResourceSet处理多个模型之间的引用的,以上面的在线商店为例,当从一个文件(一般是xmi格式)里载入一个订单实例时,EMF并不会去实际读取存储用户或商品信息的文件,而是使用代理(Proxy)的方式生成一个替代品,当程序里需要得到订单对应的用户的详细信息时(例如执行了theOrder.getCustomer().getAddress()方法),才会实际读取用户信息文件,并根据订单的用户id找到对应的用户信息。

因此,当你的项目里有多个模型并且存在相互引用的这种情况时,建议维护一个全局的ResourceSet实例,可以在程序启动时创建:

ResourceSet resourceSet = new ResourceSetImpl();

对于普通应用程序,可以用单例模式(Singleton)维护ResourceSet实例;若在web应用程序里,也可以利用application维护它。在程序里,每载入一个模型都要把它加到这个ResourceSet实例上。具体方法如下:

//get global ResourceSet instance
ResourceSet resourceSet = ;
//file to load
String filename = ;//e.g. "c:\\work\\test.xxx"
URI fileURI = URI.createFileURI(filename);
//create resource
XMIResource resource = (XMIResource)resourceSet.createResource(fileURI);
//load from file
resource.load(null);
//add resource to global ResourceSet instance
resourceSet.getResources().add(resource);
//get your model
XXXModel xxxModel = (XXXModel) resource.getContents().get(0); 
  1. 其他常用方法,以下仍以在线商店里的模型(见此帖)为例:

创建一个新产品(Product),通过ShopFacotory的createProduct()方法而不要直接new ProductImpl():

Product p = ShopFactory.eINSTANCE.createProduct();

Product p = (Product)ShopFactory.eINSTANCE.create(ShopPackage.Literals.Product);

把新创建的产品添加到商店(Shop):

Shop model = …;
Product p = …;
model.getProducts().add(p);

通过EMF反射机制得到Product类型的所有属性(非引用类型),任何一个EObject都可以通过这种方式得到其所有属性:

Product p = …;
EClass eclass = p.eClass(); 
for (Iterator iterator = eclass.getEAllAttributes().iterator(); iterator.hasNext();) {
  EAttribute attr = (EAttribute)iterator.next(); //属性,如Product#name
  Object value = p.eGet(attr); //属性值,如“walkman”
  … //do whatever with the value
}

通过EMF反射机制得到Category类型的所有引用,任何一个EObject都可以通过这种方式得到其所有引用:

Category category = …;
EClass eclass = category.eClass();
for (Iterator iterator = eclass.getEAllReferences().iterator(); iterator.hasNext();) {
  EReference ref = (EReference)iterator.next(); //引用,如Category#products
  Object value = category.eGet(ref); //引用值,如EList<Product>(一对多的情况)
  … //do whatever with the value
}

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2007/05/07/737869.html

EMF里EReference的containment和container属性

EReference的containment属性如果为true,表示目标EClass是被源EClass包含的,这是一种十分强的关系,例如汽车和车轮的关系。在ecore里,不允许包含关系形成圈,也就是说如果A包含B,B就不能再直接或间接的包含A;另外,如果作为容器的对象结束了自己的生命周期,被它包含的对象也将结束自己的生命周期。

如果一个EReference有作为EOpposite的EReference,并且后者的containment属性为true,则这个EReference的container属性为true。换句话说,一个EReference的container属性表示它指向的EClass是否为包含者。

public boolean isContainer()
{
  EReference theOpposite = getEOpposite();
  return theOpposite != null && theOpposite.isContainment();
}

最后,container属性是derived属性,所以在ecore编辑器里我们无法直接编辑一个EReference的这个属性。

参考资料

http://dev.eclipse.org/newslists/news.eclipse.tools.emf/msg01448.html

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2006/11/20/566556.html

EMF介绍系列(八、模型的验证)

在ecore模型里可以详细的定义各种类型、属性和方法,但对于像“每个类别里至少有两种产品”这样的限制就无能为力了。为此,EMF提供了一套验证框架(Validator Framework)用于解决这个问题,在ecore文件里特定的方法可以被识别为验证方法并生成用于验证的代码。

还是以shop模型为例,假设要求“每个类别里至少有两种产品”,我们需要在shop.ecore里添加一个名为validateProductsCount的验证方法,如图1所示。验证方法的返回类型是要具有两个参数:第一个是EDiagnosticChain类型,第二个是EMap类型,参数的名称没有特别要求,但这两个参数的顺序不能交换。

file
图1 新增的验证方法

接下来,通过shop.genmodel重新生成一遍代码(如果还没有shop.genmodel文件,通过New -> EMF Model创建一个),注意没有必要reload这个genmodel文件。通过比较添加验证方法前后的代码,可以发现EMF在util包里多生成了一个名为ShopValidator.java的文件;同时,在CategoryImpl的validateProductsCount()方法里的代码如下所示:

/**
 * <!-- begin-user-doc -->
 * <!-- end-user-doc -->
 * @generated
 */
public boolean validateProductsCount(DiagnosticChain diagnostics, Map contex) {
    // TODO: implement this method
    // -> specify the condition that violates the invariant
    // -> verify the details of the diagnostic, including severity and message
    // Ensure that you remove @generated or mark it @generated NOT
    if (false) {
        if (diagnostics != null) {
            diagnostics.add
                (new BasicDiagnostic
                    (Diagnostic.ERROR,
                     ShopValidator.DIAGNOSTIC_SOURCE,
                     ShopValidator.CATEGORY__VALIDATE_PRODUCTS_COUNT,
                     EcorePlugin.INSTANCE.getString("_UI_GenericInvariant_diagnostic", new Object[] { "validateProductsCount", EObjectValidator.getObjectLabel(this, contex) }),
                     new Object [] { this }));
        }
        return false;
    }
    return true;
}

有别于普通方法的实现(简单的抛出一个UnsupportedOperationException异常)。由于这段代码是被if(false){...}包围的,所以如果不进行定制,则里面的内容永远不会被执行,被包围代码的功能是将验证错误记录下来以便统一报告。现在,我们要做的就是修改这个条件,来告诉EMF什么时候运行里面的代码,如下所示:

/**
 * <!-- begin-user-doc -->
 * <!-- end-user-doc -->
 * @generated NOT
 */
public boolean validateProductsCount(DiagnosticChain diagnostics, Map contex) {
    // 我们修改了验证条件
    if (getProducts().size() < 2) {
        if (diagnostics != null) {
            diagnostics.add
                (new BasicDiagnostic
                    (Diagnostic.ERROR,
                     ShopValidator.DIAGNOSTIC_SOURCE,
                     ShopValidator.CATEGORY__VALIDATE_PRODUCTS_COUNT,
                     EcorePlugin.INSTANCE.getString("_UI_GenericInvariant_diagnostic", new Object[] { "validateProductsCount", EObjectValidator.getObjectLabel(this, contex) }),
                     new Object [] { this }));
        }
        return false;
    }
    return true;
}

因为我们要实现的验证条件很简单,所以代码的改动也很少,还是注意不要忘记修改注释中的@generated标记。现在可以运行我们的shop编辑器了,在一个只包含一种产品(Product)的类别(Category)上按右键,选择弹出菜单里的“Validate”命令,就会得到如图2所示的提示信息。

file
图2 模型未通过验证

通过修改代码的方式表达模型的限制条件是很直观,不过当条件又多又不确定的时候,直接在ecore文件里集中的表达这些条件也许更方便管理,而且Java代码甚至不需要重新生成和编译。Eclipse网站上的这篇文章“Implementing Model Integrity in EMF with EMFT OCL”通过自定义JET模板实现了这个功能,有兴趣的朋友不妨试试。

下载本文工程

补充另一种让EMF生成Validate代码的方法:在ecore模型里需要验证的EClass下建立一个EAnnotation,其source属性为“http://www.eclipse.org/emf/2002/Ecore”;然后在这个EAnnotation下建立key为“constraints”的Details Entry,value属性指定为想要的constraint名字,如果要定义多个constraint,则每个名字之间用空格分隔(格式见EcoreUtil#setConstraints()),如图3所示。这样,EMF就会在util包里生成XXXValidator.java文件,以及相应的验证方法,这些方法的代码和上面第一段嗲吗是类似的,同样需要自己修改if语句里的条件。

file
图3 在ecore模型里添加EAnnotation以生成验证代码

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2006/08/09/472607.html

关于实现StarGen的思考记录

毕业设计的内容是Web应用程序的代码生成器,因为接触emf有一段时间了,我觉得用emf完全可以很方便的实现这个程序。这是更全面了解emf特别是codegen部分的一个好机会。这个帖子将记录这个过程的点滴,所以会比较琐碎,也许这些文字能被用在毕业论文里(不希望论文里都是google来的东西)。我发现cnblogs的帖子被修改后在rss里会重新出现,所以订阅我的blog的读者可能要经常被“骚扰”了:P

为了方便起见,我给这个代码生成器起了一个名字叫做“StarGen”,完全是一个代号,不包含任何意义。而且很幸运,这个名字在sourceforge.net上没有被注册过,等有了时间我要申请一下,所以程序里的包名都先以net.sf.stargen开头了。(请不要和我抢注这个名字啊,否则光refactor的工作就不少了,也许我就不开源了。)

闲话少说,以下是在实现StarGen过程中对一些问题的思考:

1、(3-24)首先我从使用者的角度审视一下这个应用程序完成后应该具有的功能,StarGen的使用者其实就是Web应用的开发者,他通过StarGen可以定义一些元素,这些元素将被转换为代码。粗略想了一下,可以定义的元素包括Class、Attribute、Operation、Reference等等,很像是一张类图。每个类将被转换为Web应用的一个模块,包含增、删、改、查询、列表、查看这些功能,这些功能分别由一个或多个jsp页面和Struts的Action(目前考虑生成基于Struts和Hibernate的应用,因为实验室一直使用它们)。

那么是否有了类图就够了呢,这样只能实现简单的数据操作,基本没有什么业务逻辑在里面。为了能让生成的Web应用具有逻辑性,我初步为StarGen的输入除类图以外增加了一个流程图,流程图里定义的流程会在生成的代码里得到体现。这个图和工作流里的那种图还是要有所区别的,我以前开发过工作流程序,在流程引擎里都要处理条件、会签、回退等情况,实际上是非常复杂的。因此流程这部分还需要花时间考虑一下怎样简化比较合适。

file
图1 初步的StarGen使用步骤(六个步骤)

2、(3-24)既然明确了StarGen的输入,一个是类图,另一个是暂时称作流程图的东西,那么使用者应该怎样提供这些输入呢?首先看一下类图方面,这里使用者至少要能够定义出应用里应该有哪些对象类型,它们的成员变量、方法等等,还有类与类之间的联系。如果说他(使用者)定义的是一个模型,那么我首先要有一个元模型,元模型好比模型的语言,在元模型里我要定义“类”、“成员变量”、“方法”等等这些概念。按照这个想法,我想到平时使用emf建模时用的“语言”不就是ecore吗,没错,ecore是一个元模型,和uml一样,都可以用来定义模型。那么我可不可以就让使用者直接用ecore建模呢,因为使用ecore建模的工具已经有不少了,最起码也有随emf一起来的“Sample Ecore Model Editor”,想图形化可以用EclipseUML或者gmf的ecore editor都行,这样我可以节约很多工作量。

3、(3-24)图1中步骤3和步骤5是StarGen的主要工作所在。就像emf里有genmodel一样,StarGen也需要有自己的genmodel,不妨暂时称作“StarModel”吧。使用者输入的两个model被合并为一个starmodel,在starmodel里包含了它们的全部信息,此外还有一些如包名、类名这样的信息,这些信息和Web应用的业务逻辑没有什么关系。生成starmodel的部分我将主要org.eclipse.emf.importer这个项目,它可以从包括ecore模型在内的几种模型里生成genmodel。

另一项重要工作就是从starmodel生成可以执行的代码,这里我将参考以org.eclipse.emf.codegen开头的四个项目,特别是Generator类,我会从它“开刀”看看有什么值得利用。另外org.eclipse.uml2.codegen开头的几个项目也值得一看,uml2的代码生成借用了很多emf的东西,例如uml2的genmodel就完全是从emf的genmodel继承来的,只是增加了一些新的元素,我可能也会采取这种方式而非从头开始定义一个和emf的genmodel类似的starmodel。BTW,我觉得这不是走捷径,这叫“Leverage”!

4、(3-27)在emf里,从ecore生成genmodel的主要工作是在EcoreImporter中完成的,这个类继承ModelImporter类。在模型导入向导ModelImporterWizard的performFinish()方法里会调用prepareGenModelAndEPackages()和saveGenModelAndEPackages()方法,前者负责构造genmodel实例,后者负责保存这个实例。

ModelImporter的prepareGenModelAndEPackages()方法的主要过程是这样的:计算这个genmodel引用到的其他genmodel,将这些genmodel连同自己都加到同一个Resource内,调用genmodel的initialize()方法,这个方法会导致genmodel里的GenPackage的initialize()方法被调用,再导致GenClassifier的initialize()方法被调用,如此类推,直到整个genmodel构造完毕。然后,通过调用traverseGenPackages()和adjustGenModel()对得到的genmodel做一些调整,主要是一些属性的赋值,traverseGenPackages()的作用还不太明白。最后,调用reconcile()方法将原来的genmodel里被修改过的值应用到新生成的genmodel上,否则用户在Genmodel Editor里所做的修改在reload一次后就丢失了。

ModelImporter的saveGenModelAndEPackages()则十分直白,它会判断是否需要新建project,以及根据需要reference的其他genmodel决定在project里增加对哪些project的依赖(一般情况下好像都是在已有project里建立genmodel的),然后对需要保存的resource调用Resource#save()即可。

对于StarGen来说,保存的那部分应该是一样的,主要的修改会在生成genmodel的prepareGenModelAndEPackages()方法里,因为StarGen的genmodel(StarModel)在emf的genmodel基础上增加了Process等类,所以很可能需要在StarPackage的initialize()里对它们进行处理,这通过覆盖GenPackage()的initialize()方法就可以实现。

5、(3-29)EMF Model的新建向导(File->New->Others->Eclipse Modeling Framework->EMF Model)对应的类是EMFModelWizard,整个向导由两部分构成,第一部分是前两页,第一页叫NewGenModelFileCreationPage,在这里用户指定.genmodel文件的名字和所在项目,第二页是一个ModelConverterDescriptorSelectionPage的子类,它搜索注册到Eclipse上的org.eclipse.emf.importer.modelImporterDescriptors扩展点提供一个ModelImporter列表,例如EcoreImporter、RoseImporter等等。注意,在ModelConverterDescriptorSelectionPage的getNextPage()方法里会决定使用哪个Wizard作为向导第二部分,同时会调用adjustModelConverterWizard()方法做一些赋值工作。

向导的第二部分就由Importer决定,对于Ecore的情况对应EcoreImporterWizard,它是ModelImporterWizard的子类,包含两个向导页ModelImporterDetailPage和ModelImporterPackagePage,分别让用户选择一个.ecore文件和选择要生成或引用的包。ModelImporterWizard的子类都要实现createModelConverter()方法,返回一个ModelConverter类的实例,例如return new EcoreImporter(),EcoreImporter是ModelImporter的子类,ModelImporter则是ModelConverter的子类。

向导负责收集必要的信息,具体的生成.genmodel文件的工作在ModelImporter里完成,主要的步骤就是前面说过的prepareGenModelAndEPackages()和saveGenModelAndEPackages()方法。

6、(3-30)为了实现StarGen区别于emf的功能(流程等等),我“继承”emf的genmodel模型(genmodel.ecore)构造了StarGen的模型(stargen.ecore),这个过程参考了uml2对codegen的实现。StarGen的genmodel如下图所示:

需要注意的是,图中很多元素都要继承两个类,一个是emf中的GenXXX,另一个可能是StarBase,StarBase是GenBase的子类。这时要把GenXXX放在继承列表的第一位,否则生成的代码会缺少一些方法如getName()和reconcil()而无法编译通过。另外一点要注意的就是,在genmodel.genmodel(由genmodel.ecore生成,位于org.eclipse.emf.codegen.ecore/model)中,GenModel的Creation Commands属性是false,继承的stargen.genmodel里也要做同样的设置,否则在生成的editor里会出现MissingResourceException。不过,这样设置以后editor里就没有New Child/Sibling菜单选项了。

7、(3-31)初步使用自己的Template生成代码。实际上emf从大约2.2.0开始支持了Dynamic Template,只要把genmodel的“Dynamic Templates”属性设置为true,并在适当的位置放置另外的Template即可按这个Template生成代码。不过,为了更大的灵活性以及我暂时没有想到的情况,我没有使用这种机制。在emf的GenClassImpl这个类里有很多getXXXEmitter()方法,它的返回值决定了genmodel里各个元素使用哪个模板,例如GenClassImpl的getFactoryClassEmitter()是这样写的:

 public JETEmitter getFactoryClassEmitter()
  {
    if (factoryClassEmitter == null)
    {
      factoryClassEmitter = createJETEmitter(factoryClassTemplateName);
      setMethod(factoryClassEmitter, "org.eclipse.emf.codegen.ecore.templates.model.FactoryClass");
    }
    return factoryClassEmitter;
  } 

可以看到emf生成Factory类是通过org.eclipse.emf.codegen.ecore.templates.model.FactoryClass这个(经过编译的)模板完成的,如果想使用自己的模板,只要在StarModelImpl类里覆盖这个方法(StarModelImpl是GenModelImpl的子类,参考前面stargen的模型图),把setMethod()的参数改为"net.sf.stargen.templates.FactoryClass"即可。

模板是一些.javajet文件,在一个具有jet nature的project里,你写的所有.XXXjet文件在保存时都会被编译为一个.java类,该类的generate()方法生成.XXX文件的内容。emf的那些.javajet文件的内容还是比较复杂的,需要花些时间看看怎么利用,其中最主要是Class.javajet,因为StarGen不会去生成.edit和.editor的代码。再接下来就是要构造与Web应用有关的Template了,例如Action.javajet或是Mapping.hbm.xmljet等等,工作量也许很大,但一些问题应该可以先简化处理。

8、(4-5)现在StarGen可以从.ecore文件生成.starmodel文件了,并且可以生成一个Factory和n个bean-like类(n=ecore里EClass的数量),我对emf的Class.javajet做了巨大的简化,代码从1800多行减为不到100行,汗,真难想象emf的Class.javajet当时是怎样写出来的。下一步的工作是生成struts的配置文件,这里有一个之前没有考虑到的困难,那就是在xml文件里怎样控制jmerge的行为,应该与在java类里有所不同,还是要学习一下。

9、(4-10)生成.jsp文件的时候遇到一些困难。一是jsp与jet的控制标签都是"<% %>",必须在jet模板里指定使用不同的标签,例如startTag="<$" endTag="$>";二是GenBaseImpl类提供了多个generate()方法,用于生成.java文件的那个generate()方法不能用于生成.jsp文件,因为输出的文件名的扩展名在方法里是写死为".java"的,要使用具有五个参数的那个generate()方法,并自己计算输出文件的全名。

10、(4-11)StarGen生成如下代码:

模型元素 生成的代码
StarModel web.xml, hibernate.properties
StarPackage Factory, struts-config.xml, Resource.properties(4-17), IndexAction(4-15), index.jsp(4-15)
StarClass Bean-like Class/Enum(4-18), Helper(4-14新增), XXXAction, XXXForm, JSPs(List, Create, Edit), struts-config-xxx.xml, ApplicationResources.properties, xxx.hbm.xml, validation.xml(4-18)

接下来要花几天的时间对模板内容进行调整,这样就可以初步得到可以运行的生成结果。(PS.昨天看到一个很像的代码生成工具Modelstry,也是生成基于struts的web应用,还利用了spring来管理hibernate。但它好像不是用jet生成的代码,不知道merge方面是否有问题。刚下载了试用版本,有时间研究一下,取长补短。)

11、(4-13)生成了可以列表和添加新记录的web应用程序。第三方类库,包括Struts和Hibernate用到的那些jar包和tld文件,则需要用户手动添加到项目。GenFeature有一个方法是getGetAccessor(),得到的是bean里的getter方法名字,但却没有getSetAccessor(),为什么呢,因为getter不一定是以"get"开头的,还有"is"开头的情况,所以在getGetAccessor()里做了处理,而需要setXXX方法的地方直接写set<%=genFeature.getAccessorName()%>即可。

12、(4-14)实现了简单类的编辑和删除,jsp和action。为每个ecore类新生成一个Helper类,功能类似emf生成的ItemProvider,目前主要提供getText()方法,用户可以修改这个方法显示需要的内容。缺省的这个类返回<%=genClass.getSafeUncapName()%>.<%=genClass.getLabelFeature().getGetAccessor()%>()。

13、测试用的ecore模型,十分简单。

file

14、(4-17)处理了jsp页面里用checkbox显示GenFeature#isBooleanType()返回true的类型,在StarFeature里增加了password布尔变量,此值为true的用代替。非String类型的GenFeature,如int型和boolean型,不需特殊处理即可正常使用,原因是在ActionForm里已经是这种类型,本来担心的在Action的postCreate()/postEdit()方法里需要parse的问题并不存在。

15、(4-18)对特殊的GenFeature(即不是普通String类型的属性)做如下处理:

GenFeature情况 处理方法
transient 在hibernate映射文件里去掉对这类属性的声明
volatile 待处理
boolean 在create.jsp/edit.jsp页面中以checkbox展示
contains 在container的编辑和查看页面中增加一个创建child的link
ListType 在create.jsp/edit.jsp页面中以multiple的select展示,在hibernate映射文件里使用代替
reference   在hibernate映射文件里使用代替
MapType 这一版本暂时不作处理
Bidirectional 在hibernate映射文件里设置,在一方的column名称与一方的column保持一致
EnumType 假设GenFeature名为location,在模型类里增加一个int型的locationValue属性,mapping里只对这个属性持久化,而getEnum()里通过locationValue计算得到Enum类型返回值。生成继承AbstractEnumerator的类,生成一个Resource文件(为了对Enumerator的Literals实现国际化),在具有Enum类型成员变量的GenClass生成的struts-config-xxx.xml里要引用这个Resource文件。在Create.jsp/Edit.jsp里用下拉框展示,View.jsp/List.jsp里显示Resource里对应的文字内容(而非直接显示Literal),ActionForm里与模型类相对应,要有locationValue属性。(4-19)
notNull 在hibernate映射文件里设置 ,在validation.xml文件里增加required约束
其他非String类型 在validation.xml文件里增加相应约束

16、(4-18)GenFeature常用的方法:getTypeGenClass()得到与GenFeature关联的“对面”的GenClass;

17、(4-19)让生成的java代码能够拥有适当的import声明的方法:在jet文件前部增加类似下面的语句:

<%genModel.addImport("java.util.Arrays");%>
<%genModel.addImport("java.util.List");%>
<%genModel.addImport("java.util.Collections");%>
<%genModel.markImportLocation(stringBuffer, genPackage);%>

再在jet文件最后一行增加<%genModel.emitSortedImports();%>这句即可。

 18、(4-19)实现了对Enum类型GenFeature的处理,但生成的代码并不完美,在JSP页面里有类似下面的语句(因为用struts的标签无法很方便的表示,所以用

<html:select property="locationValue">
    <%for (Iterator iter = Location.VALUES.iterator(); iter.hasNext();) {
        Location location = (Location) iter.next();%>
    <option value="<%=location.getValue()%>"
        <bean:message key="<%=location.getName()%>" bundle="location"/>
    </option>
    <%}%>
</html:select>

19、(4-20)生成的列表支持排序了。下面要处理的是分页问题,会稍微麻烦一些。

20、(5-10)针对流程的设计:流程(Process)由活动(Activity)组成,每个活动可以等价为对数据库的一个操作,例如增加一个用户就是在用户表里增加一条记录;又因为使用了Hibernate,所以对数据库的操作就是对持久对象的操作,例如增加用户就是增加一个User对象。

为了简化实现,暂时不考虑在流程中引入条件。

每个活动应该具有以下属性:

  • 操作(Operation),枚举类型,可选值为Select、Create、Delete、Modify之一;在实现中也可以使用活动不同的子类达到同样目的。
  • 目标类型(TargetType),它的值是数据模型里的一个类,例如User、Product,等等;因此流程模型有对数据模型的依赖。
  • 目标对象(TargetObject),该活动中处理的实例,例如用户张三;

活动间目标对象的传递问题:例如第一个活动创建了Product1,第二个活动的内容是编辑Product1的属性,在流程设计中,如何告知第二个活动Product1的ID号?另外,会不会存在有多个ID号需要告知的情况?现在的想法是在session里维护这个对象,Select操作为这个对象赋值,流程结束时对其清空。

21、(5-11)因为流程里没有分支结构,所以Begin和End这两个Activity似乎没有必要存在了。今天完成了StarFlowModel的原始编辑器,用它创建的流程模型可以通过“Load Resources...”载入数据模型,并建立两个模型之间的关联。同时删除了原来设计在数据模型里的Process和Activity这两个类型。

接下来要考虑具体的流程模板内容了。

22、(5-13)基本完成了Select和Modify这两个活动的模板,利用它们可以组成“修改产品信息”这样的简单流程。剩下的活动类型还有Create、Delete和Custom这几种,其中Custom将生成没有任何功能的代码框架,让用户自己实现任意功能。

另外,流程对应的模板也已经完成了。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2006/03/24/357784.html

关于Free版的EclipseUML

Omondo EclipseUML分为Studio版和Free版两种,我只用过Free版,对于创建EMF类图来说感觉已经够用了。不过和Eclipse的版本比起来,EclipseUML的升级比较缓慢,目前为止最新的版本还是2005年9月27日放出的,这就造成在新版本Eclipse里EclipseUML可能无法正常运行。

20050927版本是针对Eclipse 3.1开发的,现在Eclipse已经出到M5版本,我在Eclipse 3.2M4里运行这个版本的EclipseUML基本没有出现问题,但到了M5就出现了不能创建Enumeration的问题,表现为无法添加Enum项,所报异常如下:

java.lang.NoSuchMethodError: org.eclipse.jdt.internal.ui.wizards.dialogfields.ListDialogField.addElement(Ljava/lang/Object;)V
    at com.omondo.uml.obf.bte.a(SourceFile:352)
    at com.omondo.uml.obf.bte.a(SourceFile:63)
    at com.omondo.uml.obf.yt.customButtonPressed(SourceFile:315)
    at org.eclipse.jdt.internal.ui.wizards.dialogfields.ListDialogField.buttonPressed(ListDialogField.java:204)
    at org.eclipse.jdt.internal.ui.wizards.dialogfields.ListDialogField.doButtonSelected(ListDialogField.java:458)
    at org.eclipse.jdt.internal.ui.wizards.dialogfields.ListDialogField.access$0(ListDialogField.java:454)
    at org.eclipse.jdt.internal.ui.wizards.dialogfields.ListDialogField$2.widgetSelected(ListDialogField.java:420)
    at org.eclipse.swt.widgets.TypedListener.handleEvent(TypedListener.java:90)
    at org.eclipse.swt.widgets.EventTable.sendEvent(EventTable.java:66)
    at org.eclipse.swt.widgets.Widget.sendEvent(Widget.java:1074)
    at org.eclipse.swt.widgets.Display.runDeferredEvents(Display.java:3158)
    at org.eclipse.swt.widgets.Display.readAndDispatch(Display.java:2838)
    at org.eclipse.jface.window.Window.runEventLoop(Window.java:811)
    at org.eclipse.jface.window.Window.open(Window.java:789)
    at com.omondo.uml.obf.ahk.c(SourceFile:167)
    at com.omondo.uml.obf.le.d(SourceFile:572)
    at com.omondo.uml.obf.fkr.execute(SourceFile:86)
    at org.eclipse.gef.commands.CommandStack.execute(CommandStack.java:149)
    at org.eclipse.gef.tools.AbstractTool.executeCommand(AbstractTool.java:388)
    at org.eclipse.gef.tools.AbstractTool.executeCurrentCommand(AbstractTool.java:400)
    at org.eclipse.gef.tools.CreationTool.performCreation(CreationTool.java:254)
    at org.eclipse.gef.tools.CreationTool.handleButtonUp(CreationTool.java:178)
    at org.eclipse.gef.tools.AbstractTool.mouseUp(AbstractTool.java:1053)
    at org.eclipse.gef.EditDomain.mouseUp(EditDomain.java:259)

在为了提高兼容性而特别制作的Eclipse 3.2M5a版本里也是同样的现象。不过已经创建好的Enumeration在Eclipse M5下则显示正常。因此提醒大家在使用这个插件的时候要特别注意多备份自己的ecd和ecore文件,以免造成不可挽回的后果。

另外,Omondo最近可能会有新的版本放出了,毕竟已经过了半年这么长的时间。很多朋友反映上不去www.omondo.com的问题,其实用国外代理是可以上的,不知道为什么这个网站也被禁掉了。Update(2012/11/12): 今天测试omondo网站已经可以从国内直接访问了。

补充:Omondo EclipseUML是支持cross reference的,见这个链接

补充:在EclipseUML的类图编辑器里删除一个类的时候要特别小心,如果其他类有以这个类为返回值或参数类型的时候,这个类图保存关闭后将无法再次打开

EclipseUML定义枚举项时慎用符号

今天用EclipseUML画类图遇到一个很郁闷的问题,为了保险起见我还是边画边保存的,画了一上午,有一次关闭了编辑器,再想打开时提示“Impossible to load the diagram xxx.ecd”。我这汗一下子就下来了,一上午的工作啊!赶紧查看一下Eclipse的errorlog,异常信息如下:

java.lang.NullPointerException
    at com.omondo.uml.obf.cba.setInput(SourceFile:1153)
    at com.omondo.uml.obf.cba.init(SourceFile:1109)
    at com.omondo.uml.emf.ClassDiagramEditor.init(SourceFile:1013)
    at org.eclipse.ui.internal.EditorManager.createSite(EditorManager.java:784)
    at org.eclipse.ui.internal.EditorReference.createPartHelper(EditorReference.java:585)
    at org.eclipse.ui.internal.EditorReference.createPart(EditorReference.java:374)
    at org.eclipse.ui.internal.WorkbenchPartReference.getPart(WorkbenchPartReference.java:552)
    at org.eclipse.ui.internal.PartPane.setVisible(PartPane.java:285)
    at org.eclipse.ui.internal.presentations.PresentablePart.setVisible(PresentablePart.java:140)
    at org.eclipse.ui.internal.presentations.util.PresentablePartFolder.select(PresentablePartFolder.java:264)
    at org.eclipse.ui.internal.presentations.util.LeftToRightTabOrder.select(LeftToRightTabOrder.java:65)
    at org.eclipse.ui.internal.presentations.util.TabbedStackPresentation.selectPart(TabbedStackPresentation.java:394)
    at org.eclipse.ui.internal.PartStack.refreshPresentationSelection(PartStack.java:1140)
    at org.eclipse.ui.internal.PartStack.setSelection(PartStack.java:1093)

找不到任何与类图有关的信息,试着修改.ecd和.ecore文件,几次均没有效果。想到昨天晚上做过备份(万幸),只好恢复到那时的类图重新画。画完了保存,试着关闭编辑器再打开,竟然又提示“Impossible to load the diagram xxx.ecd”!!

我快不行了,最后的办法,边画边保存边备份,想看看到底是什么操作引起的这个问题。终于找到根源了,原来是我定义了一个名为Operator的Enumeration,里面有“+”、“-”、“*”和“/”四个枚举项,其中的“/”号会引起.ecd文件失效,我猜想是没有转义的缘故,EclipseUML会把它认作分隔符。

最后建议大家一定要像对自己的眼睛一样爱护自己的模型,经常备份是非常必要的,现在免费的图形化编辑器普遍都不是很稳定。

EMF介绍系列(七、.Edit初步)

EMF除了生成模型部分的接口和实现类(不妨称作“核心模型”)以外,还生成一个名称以.Edit结尾的项目,包含一些与核心模型和编辑器关系都十 分紧密的代码。这部分代码经过了精心设计,可重用的程度是相当的高。它们不仅在EMF生成的编辑器项目里大量被用到,我们自己在扩展编辑器的时候也应该充 分利用。

在线商店的例子里,com.my.shop.edit项目里包含一个ItemProviderAdapterFactory类和一组 ItemProviderAdapter的子类,后者是和核心模型的接口一一对应的,例如核心模型的Shop、Category和Product分别对应 ShopItemProvider、CategoryItemProvider和ProductItemProvider。这篇帖子主要介绍一下这些 ItemProvider,而关于ItemProviderAdapterFactory的内容将在以后的帖子里专门介绍,其实顾名思义, ItemProviderAdapterFactory的作用主要就是生成ItemProvider。事实上在构造EMF应用程序时,我们经常要修改 ItemProvider里的代码,而ItemProviderAdapterFactory则很少改动。

file
图1 EMF生成的.Edit项目

注意:.Edit项目里ItemProviderAdapter的子类名称里省略了Adapter这个单词,例如 CategoryItemProvider而非CategoryItemProviderAdapter,你心里应该清楚它是一个Adapter,因为它 确实实现了Adapter接口。EMF里另外专门有一个ItemProvider类是为非Adapter类型准备的,在这篇里说的 ItemProvider不是指它,而是指XXXItemProvider,也就是ItemProviderAdapter的子类。

注意:EMF里的Adapter接口和Eclipse Runtime的IAdaptable接口虽然名称相似,但并不是同一个概念(关于IAdaptable请参见前面的翻译帖子), EMF里的Adapter等同于监听器(Listener、Observer)的作用,它监听的对象是EMF的Notifier,在一个Notifier 上可以注册多个Adapter。另一方面,ItemProviderAdapterFactory则很像IAdaptable,它们都能够起到动态转换类 型的作用,只不过前者一般只用于Notifier到Adapter的转换,后者则没有什么限制,此外转换方法的名称也不同,前者是adapt(),后者为 getAdapter()。

从图1中不难看出,ItemProvider构成了.Edit项目的主要部分,这些ItemProvider具有以下几个作用。

一、实现了JFace中ContentProvider和LabelProvider的功能

JFace查看器(Viewer)是对swt中控件的一种包装,例如TableViewer是对Table的包装,TreeViewer是对Tree的包 装,等等,通过这种方式可以将控件与显示在控件中的数据在一定程度上分离,从而方便数据显示的更新。相当多的Eclipse应用程序都是通过JFace查 看器显示数据的,与查看器关联的ContentProvider和LabelProvider分别控制查看器中显示的哪些数据以及每条数据的显示方式。

以TreeViewer的ContentProvider为例,在JFace里应该实现ITreeContentProvider接口,这个接口定 义了getParent()、hasChildren()和getChildren()这三个方法;在EMF里有 ITreeItemContentProvider接口与之对应,这个接口同样具有这三个方法,.Edit部分的每个ItemProvider都实现了这 个接口,因为EMF已经完全知道我们的模型结构,所以这三个方法在ItemProviderAdapter类里已经实现好了。不过 ITreeItemContentProvider毕竟不能直接交给JFace的TreeViewer来使用,所以EMF提供了一个 AdapterFactoryContentProvider来做适配工作,你可以在编辑器的代码里看到如何使用它。

LabelProvider也是类似的,它主要控制显示的文字和图标。EMF生成的ItemProvider缺省没有实现 ITableItemLabelProvider,所以如果要使用TableViewer,要修改代码以实现 ITableItemLabelProvider接口和额外的方法,具体请参考在线商店例子中的ProductItemProvider。从 JFace的角度来说,ItemProvider相当于集成了各种查看器的ContentProvider和LabelProvider的代码,是一个通 用的“ContentLabelProvider”。因此利用它,开发人员在改变查看器的时候只需要修改很少的代码,而不像传统方式那样每换一个查看器还 要写新的ContentProvider和LabelProvider。

二、提供了关联对象的属性表

每个ItemProvider的getPropertyDescriptors()方法返回在属性视图里显示的属性列表,列表里的每个元素是一个 ItemPropertyDescriptor对象,它决定了每个属性的标签、描述、图标以及是否可编辑。EMF为生成的代码会帮我们把模型定义里的每个 属性都显示在属性列表里,如果希望隐藏某些属性,可以通过修改这个方法移除之。

以Product为例,ProductItemProvider的getPropertyDescriptors()方法里包含这样六条语句,分别代表产品名称、价格、描述、是否有货、评价以及颜色这六个属性,如果你想让颜色属性在属性列表里消失,只要删除最后一句即可。

addNamePropertyDescriptor(object);
addPricePropertyDescriptor(object);
addDescriptionPropertyDescriptor(object);
addAvaiablePropertyDescriptor(object);
addScorePropertyDescriptor(object);
addBackgroundPropertyDescriptor(object);

三、生成编辑模型的各种命令

在ItemProviderAdapter基类里有很多createXXXCommand()方法,如果你用过GEF应该对这些名称不陌生,因为在 EditPolicy里也有类似的方法。我们知道,为了实现Undo/Redo功能,对模型的每个改变都应该使用Command实现,然后把 Command保存在Command栈里,每个Command对象保存Undo/Redo自己的信息。ItemProviderAdapter相当于一个 生产这些Command的工厂,用户对模型编辑的请求都将通过它转换为对应的Command,例如用户在属性视图里修改了一个属性的值,当按下回车后,会 调用该对象关联的ItemProvider类的createSetCommand()方法生成一个SetCommand对象。

注意:在createCommand()方法里会调用getChildrenFeatures()方法,而在实现ContentProvider的getChildren()时也需要这个方法,因此这个方法的返回结果同时影响ItemProvider的这两项功能。

四、将模型的改变通知到负责显示模型的视图

在一个Eclipse应用程序里经常会有很多个查看器显示模型,无论用户怎样修改模型,要让这些查看器里显示的内容总是当前的模型,最好的办法是让查看器能够响应模型的变化。ItemProvider作为监听器可以很好的完成这个任务。

模型发生改变时,与被修改的对象相关联的ItemProvider的notifyChanged()方法被调用,事件立即被通知给 ItemProviderAdapterFactory,后者是整个模型的事件处理机构,所有的ItemProvider都是通过 ItemProviderAdapterFactory创建并注册为监听器的,因此ItemProviderAdapterFactory可以把事件通过 fireNotifyChanged()通知给所有这些监听器的notifyChanged()方法去消化。图2展示了这个通知过程,此图来自 《Eclipse Modeling Framework: A Developer's Guide》第3.2.4节中的图3.10。

file
图2 ItemProvider的通知流程

最后,ItemProvider还有一个collectNewChildDescriptors()方法,这个方法决定了在编辑器里,模型里对应的 那个对象可以创建哪些子元素。例如在线商店模型里,Category对象的子元素是Category和Product,那么用户在编辑器里右键点击一个 Category对象选择“New Child”时,就会出现“Category”和“Product”这两个选项。有些场合我们想隐藏其中一些选项时,就可以修改这里的代码。

参考资料

Eclipse Modeling Framework A Developers Guide,第3.2节、第10.1节。

搬家前链接:https://www.cnblogs.com/bjzhanghao/archive/2006/02/02/324909.html