从 XMLBeans 接收事件,定制 XMLBean POJO,把数据模型的更改通知观察器

XML 模式描述 XML 的类型、元素和结构。Simple API for XML(SAX)、Document Object Model(DOM)和 XML Object Model(XOM)等一般性工具很难简便地使用这些信息。XMLBeans 这种数据绑定框架可以从 XML 模式创建 POJO,允许快速地读取、操作和写 XML。

在处理 POJO 时,尤其是胖客户机,需要知道对象什么时候发生变化 — 通常称为事件(eventing)通知(notification)。事件是 Model View Controller(MVC)和 Model 2 模式的核心组件,这些模式的主要意图是在图形用户界面(GUI)代码和模型代码之间形成松散耦合。我们将以 Sudoku 游戏为例,讲解如何用 XMLBeans 扩展在基于 XML 的 Java 应用程序中添加事件功能。

Sudoku 是一个 9 乘 9 的网格,它划分为 9 个 3 乘 3 的子网格。在游戏开始时,一些单元格已经填上了 1 到 9 的数字。玩家的任务是根据以下限制填写余下的单元格:

  1. 所有单元格必须包含 1 到 9 的数字。这由 XMLBeans 生成的代码处理。
  2. 每行中的所有 9 个单元格必须是惟一的。
  3. 每列中的所有 9 个单元格必须是惟一的。
  4. 每个子网格中的所有 9 个单元格必须是惟一的。

图 1 给出 Sudoku 的游戏板。我们开发了一个应用程序,它使用 XMLBeans 事件功能检验是否满足限制 2-4。如果不满足某个限制,就更新视图,把与限制冲突的单元格显示为红色。


图 1. Sudoku 游戏板示例
Sudoku 游戏板示例

关于 Sudoku 的历史和策略的更多信息参见 参考资料

过程流概述

Observer 设计模式可以把更改通知给对此感兴趣的对象。本文中描述的 XMLBeans 事件代码是这种设计模式的实例。这种模式的思想是,允许对象进行注册,请求在运行时获得 XmlObject 中模型更改的通知。按照这种方式,就可以在 Sudoku 单元格上连接监听器,对每个更改进行检验。

XMLBeans 允许使用一个配置文件(也称为 XSDCONFIG 文件)定制代码生成过程。XSDCONFIG 文件包含以下方面的 XML 配置:

  1. Java 包的名称空间映射
  2. Java 类的类型和元素 QName 映射
  3. 在生成的代码中添加方法
  4. 在模型更改之前和之后添加实现

如果在编译时知道对更改感兴趣的对象,第 4 项 就支持为它们生成事件。但糟糕的是,通常不会提前知道这些对象。关于 XSDCONFIG 文件内容的更多信息参见 参考资料

为了启用事件,需要实现 第 3 项第 4 项。这需要完成以下四个步骤:

  1. 编写一个接口扩展,从而允许向 XmlObject 注册监听器。
  2. 编写一个 PrePostSet 扩展,它把模型更改通知监听器。
  3. 创建一个包含必要配置信息的 XSDCONFIG 文件。
  4. 把关于 XSDCONFIG 文件的信息告诉 XMLBeans 实用程序 scomp

编写一个接口扩展

XMLBeans 的接口扩展特性允许在生成的 POJO 中添加具有定制实现的方法。XMLBeans 要求创建一个接口,并为这个接口中的每个方法提供静态的实现。我们创建的接口放在 com.ibm.wsrc.xmlbeans.IModelChangeEmitter 中,它包含四个方法,见 清单 1


清单 1. com.ibm.wsrc.xmlbeans.IModelChangeEmitter.java

public   interface  IModelChangeEmitter  {
  
public void fireModelChangeEvent(ModelChangeEvent event);
  
public void addModelChangeListener(IModelChangeListener modelChangeListener);
  
public void removeModelChangeListener(IModelChangeListener modelChangeListener);
  
public boolean hasModelChangeListeners();
}

这个接口有两个生命周期管理监听器方法, addModelChangeListener()removeModelChangeListener();一个监听器检测,hasModelChangeListeners(),以及一个实用程序方法,fireModelChangeEvent(),这个方法用来把模型更改通知注册的所有监听器。

当任何过程调用这些方法之一时,XMLBeans 把调用这个方法的 XmlObject 和参数传递给一个静态实现。在这个示例中,静态实现放在 com.ibm.xmlbeans.eventing.ModelChangeEmitterHandler.java 中。因为这是一个静态实现,而且不能在 XmlObject 中存储实例变量,所以需要在别处存储对更改感兴趣的对象的列表。在这个实现中,列表存储在一个静态 HashMap 中。清单 2 给出这个类中的 Java 代码内容。


清单 2. com.ibm.wsrc.xmlbeans.ModelChangeEmitterHandler.java

public   class  ModelChangeEmitterHandler  {
  
private static HashMap<XmlObject, LinkedList<IModelChangeListener>> listeners =
    
new HashMap<XmlObject, LinkedList<IModelChangeListener>>();

  
public static void fireModelChangeEvent(XmlObject xo, ModelChangeEvent event) {
    LinkedList
<IModelChangeListener> list = listeners.get(xo);
    
if (list == null)
      
return;

    
for (IModelChangeListener listener : list)
      listener.modelChange(event);
  }


  
public static void addModelChangeListener(XmlObject xo,
    IModelChangeListener modelChangeListener) 
{
    LinkedList
<IModelChangeListener> list = listeners.get(xo);
    
if (list == null{
      list 
= new LinkedList<IModelChangeListener>();
      listeners.put(xo, list);
    }

    list.add(modelChangeListener);
  }


  
public static void removeModelChangeListener(XmlObject xo,
    IModelChangeListener modelChangeListener) 
{
    LinkedList
<IModelChangeListener> list = listeners.get(xo);
    
if (list == null)
      
return;
    list.remove(modelChangeListener);

    
// remove references of list and XmlObject
    if (list.isEmpty())
      listeners.remove(xo);
  }


  
public static boolean hasModelChangeListeners(XmlObject xo) {
    
return listeners.containsKey(xo);
  }

}

addModelChangeListener() 方法很好地演示了如何使用监听器列表。addModelChangeListener() 方法首先检查是否已经为这个 XmlObject 注册了监听器列表,如果这个列表不存在,就创建它。最后,在这个列表中添加监听器。

编写 PrePostSet 扩展

PrePostSet 扩展使我们能够在 XMLBeans 中添加定制的处理。我们使用一个静态类,其中包含在 XmlObject 更改之前和之后运行的方法,见 清单 3


清单 3. PrePostSet 扩展伪代码

if  (PrePostSetExtension.preSet())
  updateXmlObject();
PrePostSetExtension.postSet();

静态方法 preSet()postSet() 具有相同的签名:*Set(int opType, XmlObject xo, QName propertyName, boolean isAttr, int index)。初看上去,postSet() 方法似乎可以包含所有事件代码。但是,postSet() 方法并不掌握触发事件所需的所有信息。postSet() 需要向注册的所有监听器触发一个事件,事件包含更新的对象、更新的操作、更新之前的值和目前的值。在 postSet() 方法中不知道以前的值。因此,需要静态地存储以前的对象内容。

以前的值存储在静态 HashMap oldValues 中。在 preSet() 方法中,把原来的值复制到这个 HashMap 中;在 postSet() 方法中,执行实际的事件触发操作。清单 4 给出 preSet() 方法的代码。


清单 4. com.ibm.xmlbeans.eventing.ModelChangePrePostHandler.preSet()

public   static   boolean  preSet( int  opType, XmlObject xo, QName propertyName,
  
boolean  isAttr,  int  index)  {
  IModelChangeEmitter emitter 
= (IModelChangeEmitter)xo;
  
if (!emitter.hasModelChangeListeners())
    
return true;

  
// get the child that will be changed
  XmlObject oldXO = null;
  
if (isAttr)
    oldXO 
= xo.selectAttribute(propertyName);
  
else {
    
switch (opType) {
      
case PrePostExtension.OPERATION_SET:
      
case PrePostExtension.OPERATION_REMOVE:
        oldXO 
= getChildXO(xo, propertyName, index);
        
break;
      
case PrePostExtension.OPERATION_INSERT:
        
break;
    }

  }


  
// store the old value
  int hash = hashCode(xo, propertyName, isAttr, index);
  Object oldValue 
= null;
  
if (oldXO == null)
    oldValue 
= null;
  
else if (oldXO instanceof SimpleValue) {
    oldValue 
= ((SimpleValue)oldXO).getObjectValue();
    
if (oldValue instanceof XmlObject)
      oldValue 
= ((XmlObject)oldValue).copy();
    }
 else
      oldValue 
= oldXO.copy();

  oldValues.put(hash, oldValue);

  
return true;
}

在这个方法中,代码检查模型对象上是否连接了任何监听器。如果有监听器,就缓存当前的属性值。如果它不是属性,就调用实用程序方法 getChildXO()。最后,如果它是非常量 XmlObject,就复制它的值。然后,把副本或原来的值存储在 oldValues HashMap 中。清单 5 给出 postSet() 方法的代码。


清单 5. com.ibm.xmlbeans.eventing.ModelChangePrePostHandler.postSet()

public   static   boolean  postSet( int  opType, XmlObject xo, QName propertyName,
  
boolean  isAttr,  int  index)  {
  IModelChangeEmitter emitter 
= (IModelChangeEmitter)xo;
  
if (!emitter.hasModelChangeListeners())
    
return true;

  
// get the old value;
  int hash = hashCode(xo, propertyName, isAttr, index);
  Object oldValue 
= oldValues.get(hash);
  oldValues.remove(hash);

  ModelChangeEvent.Action action 
= ModelChangeEvent.Action.UPDATE;

  
// get the new xml object
  XmlObject newXO = null;
  
if (isAttr)
    newXO 
= xo.selectAttribute(propertyName);
  
else {
    
switch (opType) {
      
case PrePostExtension.OPERATION_SET:
        newXO 
= getChildXO(xo, propertyName, index);
        
break;
      
case PrePostExtension.OPERATION_INSERT:
        XmlObject[] children 
= xo.selectChildren(propertyName);

        
// on an insert, it is always the last element
        newXO = children[children.length - 1];
        action 
= ModelChangeEvent.Action.CREATE;
        
break;
      
case PrePostExtension.OPERATION_REMOVE:
        action 
= ModelChangeEvent.Action.DELETE;
        
break;
    }

  }


  
// get the new value
  Object newValue = null;
  
if (newXO == null)
    newValue 
= null;
  
else if (newXO instanceof SimpleValue)
    newValue 
= ((SimpleValue)newXO).getObjectValue();
  
else
    newValue 
= newXO;

  
// if newValue != oldValue
  if (((newValue == null&& (oldValue != null)) || ((newValue != null)
    
&& (!newValue.equals(oldValue))))
    emitter.fireModelChangeEvent(
new ModelChangeEvent(propertyName,
      emitter, oldValue, newValue, action, index));
  
return true;
}

postSet() 方法首先检查是否有任何监听器连接这个 XmlObject。如果有,代码就从 oldValues HashMap 获取原来的值。它从当前的 XmlObject 获取当前值并赋值给 newValue 变量。最后,postSet() 检查 oldValue,确定它与 newValue 不相等。XmlObject 创建并触发一个模型更改事件。清单 6 给出实用程序方法。


清单 6. com.ibm.xmlbeans.eventing.ModelChangePrePostHandler 实用程序方法

private   static  XmlObject getChildXO(XmlObject xo, QName propertyName,  int  index)  {
  XmlObject childXO 
= null;
  XmlObject[] values 
= xo.selectChildren(propertyName);
  
if (values.length != 0{
    
if (index == -1)
      childXO 
= values[0];
    
else if (index < values.length)
      childXO 
= values[index];
  }

  
return childXO;
}


private   static   int  hashCode(XmlObject xo, QName propertyName,  boolean  isAttr,  int  index)  {
  
final int prime = 31;
  
int result = 1;
  result 
= prime * result + index;
  result 
= prime * result + (isAttr ? 1231 : 1237);
  result 
= prime * result + ((propertyName == null? 0 : propertyName.hashCode());
  result 
= prime * result + ((xo == null? 0 : xo.hashCode());
  
return result;
}

getChildXO() 方法从 XmlObject 获取一个属性值。如果返回多个值,就使用 selectChildren() 方法并获取适当的子元素。hashCode() 方法为 XmlObjectXmlObject 的属性计算一个惟一的散列值;Eclipse 会自动创建这个方法。

配置 XMLBeans 的代码生成

为了在 XMLBeans 上实现事件,需要使用并配置接口扩展特性和 PrePostSet 扩展特性。清单 7 给出 XSDCONFIG 文件。


清单 7. XMLBeans 事件的 XSDCONFIG 文件

< xb:config  xmlns:xb ="http://xml.apache.org/xmlbeans/2004/02/xbean/config" >
  
< xb:extension  for ="*" >
    
< xb:interface  name ="com.ibm.xmlbeans.eventing.IModelChangeEmitter" >
      
< xb:staticHandler >
        com.ibm.xmlbeans.eventing.ModelChangeEmitterHandler
      
</ xb:staticHandler >
    
</ xb:interface >
  
</ xb:extension >

  
< xb:extension  for ="*" >
    
< xb:prePostSet >
      
< xb:staticHandler >
        com.ibm.xmlbeans.eventing.ModelChangePrePostHandler
      
</ xb:staticHandler >
    
</ xb:prePostSet >
  
</ xb:extension >
</ xb:config >

xb:config 的第一个 xb:extension 元素包含接口扩展的配置。在这里,它为生成的所有 XmlObject 定义接口来扩展 IModeChangeEmitter。XMLBeans 将运行 ModelChangeEmitterHandler 中的实现。

xb:config 的第二个 xb:extension 元素包含 PrePostSet 扩展的配置。它让 XMLBeans 在修改属性之前和之后分别调用 ModelChangePrePostHandler 类中的 preSet()postSet()

最后,为了把配置文件输入生成器,需要在 scomp 的最后一个参数中指定 XSDCONFIG 文件。对于这个 Sudoku 游戏,在一个 Java 类中指定这个参数,见 清单 8


清单 8. com.ibm.xmlbeans.generator.SudokuSchemaGenerator.java

public   class  SudokuSchemaGenerator  {
  
public static void main(String[] args) {
    String[] xmlBeanArgs 
= new String[] {
      
"-d""xb_bin",
      
"-src""xb_src",
      
"-out""lib/sudokuXB.jar",
      
"-javasource""1.5",
      
"schema/sudokuXBE.xsd",
      
"schema/eventing.xsdconfig"
    }
;
    org.apache.xmlbeans.impl.tool.SchemaCompiler.main(xmlBeanArgs);
  }

}

关于 scomp 和命令行参数的更多信息参见 参考资料

在运行这个命令之后,现在就有了来自 sudokuXBE.xsd 的 POJO,这些 POJO 在发生更改时可以通知对更改感兴趣的对象。


创建 Sudoku 游戏 RCP 应用程序

Sudoku 游戏是一个 RCP 应用程序,当用户在一个单元格中填写了不合适的值时,这个程序会通知用户。程序会根据行、列和子网格中的其他单元格检查值是否符合限制。如果有任何值不符合限制,就把单元格显示为红色,见 图 2


图 2. Sudoku 游戏
Sudoku 游戏的两个屏幕图

按照以下步骤实现这个游戏的图形用户界面(UI):

  1. 创建一个代表游戏板的 Sudoku 模式。
  2. 从这个模式生成一组包含事件功能的 XMLBeans。
  3. 使用生成的 XMLBeans 创建一个 MVC 风格的应用程序。

这个应用程序的设计原则是消除视图和模型之间的耦合。控制器是 Standard Widget Toolkit(SWT)键监听器,它首先修改模型。然后,模型把更改通知检验器。检验器检查它们控制的单元格是否是有效的;如果有效,它们设置有效标志。然后,框架触发另一个事件,视图(单元格)会捕捉这个事件。这会更新单元格的背景颜色。图 3 在一个流程图中显示这四个步骤。


图 3. Sudoku RCP 应用程序的事件流
Sudoku RCP 应用程序的事件流

在 XML 模式中对 Sudoku 游戏进行建模

模式定义很简单,首先建立游戏板。游戏板包含 9 行和 9 列单元格,单元格只能包含 1 到 9 的值。使用这个简单的定义创建模式。图 4 所示的游戏板类型由 9 个行类型的元素组成。


图 4. BoardType XML 模式类型
BoardType XML 模式类型

然后定义行类型,见 图 5。这样就足以定义整个 9 乘 9 的 Sudoku 网格。


图 5. RowType XML 模式类型
RowType XML 模式类型

行类型包含 9 个单元格类型的元素。通过把行类型划分为 9 个单元格类型的元素,就可以定义行中每个单元格中的值,见 图 6


图 6. CellType XML 模式类型
CellType XML 模式类型

单元格类型是一个简单的元素,其中可以包含 1 到 9 的值和点号(.),点号表示没有值。单元格类型还有一个属性,它根据行、列和子网格限制表示这个单元格是否有效,见 图 7


图 7. CellStringType XML 模式类型
CellStringType XML 模式类型

单元格字符串类型进一步定义单元格,把其中的值限制为 1 到 9 的值和点号。

生成 XMLBeans

通过使用这个模式定义和 配置 XMLBeans 的代码生成 中定义的 XMLBeans 生成代码,生成一组 POJO,它们代表这个模式并包含事件功能,当模型发生任何更改时,可以通知 UI。

编写应用程序 UI

由于事件功能是生成的 XMLBeans 的组成部分,所以可以按照典型的 MVC 风格编写 UI。模型的视图是使用 Eclipse 平台开发的。这个 RCP 应用程序本质上是一个 Eclipse 插件,并在这个 Eclipse 插件启动时初始化一个视图。在创建每个 SWT 文本部件并把它添加到视图中时,通过 text.addKeyListener() 方法调用添加一个键监听器。这样,当用户在空单元格中输入值时,就可以截取按键操作。见 清单 9


清单 9. com.ibm.xmlbeans.tutorial.View.addKeyListeners().new KeyAdapter() {...}

public   void  keyPressed(KeyEvent e)  {
  
switch (e.keyCode) {
    
case '1':
    
case '2':
    
case '3':
    
case '4':
    
case '5':
    
case '6':
    
case '7':
    
case '8':
    
case '9':
      
if (text.getEditable()) {
        
final CellStringType.Enum newValue =
          CellStringType.Enum.forString(String.valueOf(e.character));
        Job job 
= new Job("Run Validation"{
          
protected IStatus run(IProgressMonitor arg0) {
            XbeUtilities.getCell(Activator.getDefault().getBoard(), myRow, myCol)
              .setCellValue(newValue);
            
return Status.OK_STATUS;
          }

        }
;
        text.setText(String.valueOf(e.character));
        job.schedule();
      }

      
break;
    
case SWT.BS:
    
case SWT.DEL:
    
case '.':
    
case ' ':
      
if (text.getEditable()) {
        Job job 
= new Job("Run Validation"{
          
protected IStatus run(IProgressMonitor arg0) {
            XbeUtilities.getCell(Activator.getDefault().getBoard(), myRow, myCol)
              .setCellValue(CellStringType.X);
            
return Status.OK_STATUS;
          }

        }
;
        job.schedule();
        text.setText(
" ");
      }

      
break;
    
// ... arrow navigation code ...
  }

  e.doit 
= false;
}

然后通过 setCellValue(newValue) 调用设置模型中的值。这个调用会更新 XML 模式定义的模型。XMLBeans 事件代码触发一个事件,通知所有监听器模型已经更改了。

事件模型的关键部分是,根据单元格在行、列和子网格中的位置,通知单元格的有效性。在 modelChange(ModelChangeEvent event) 方法中,调用 runValidation() 方法来检验单元格的值,见 清单 10


清单 10. com.ibm.xmlbeans.listeners.CellValidatorListener.modelChange(ModelChangeEvent)

public   void  modelChange(ModelChangeEvent event)  {
  
if (XbeUtilities.CELLVALUE_QNAME.equals(event.getPropertyName())) {
    
// if the current cell value is set
    if (!CellStringType.X.equals(cell.getCellValue())) {
      
if (cell.equals(event.getSource())) {
        
// run manual validation
        int count = runValidation();
        numInvalid 
= count;
      }
 else {
        
// another cell
        if (cell.getCellValue().equals(CellStringType.Enum.forString(
          (String)event.getNewValue())))
          numInvalid
++;
        
else {
          
if (cell.getCellValue().equals(CellStringType.Enum.forString(
            (String)event.getOldValue())))
            numInvalid
--;
        }

      }

  }
 else if (cell.equals(event.getSource()))
    numInvalid 
= 0;

  
if (numInvalid == 0)
    cell.setValid(
true);
  
else if (numInvalid < 0{
    Activator.getDefault().getLog().log(
      
new Status(IStatus.WARNING, Activator.PLUGIN_ID,
      
"Cell validator has a negative number of invalid cells.  This is impossible."));
  }
 else
    cell.setValid(
false);
  }

}

可以看到,首先检查更改了哪个属性。在 清单 10 中,它应该是 CELLVALUE_QNAME 属性。如果这个单元格是事件源,那么调用 runValidation() 来针对所有条件执行检验。但是,如果事件源是另一个单元格,那么只需检查值是否不等于新值。如果相等,就需要把 numInvalid 加 1。如果值等于原来的值,就需要把 numInvalid 减 1。最后,如果 numInvalid 是零,就可以通过 cell.setValid(true) 把单元格的有效标志设置为 true。否则,需要通过 cell.setValid(false) 把单元格的有效标志设置为 false。通过这些调用设置单元格的有效标志之后,XMLBeans 事件机制触发另一个事件。视图监听器处理这个事件,更新单元格的背景颜色,见 清单 11


清单 11. com.ibm.xmlbeans.tutorial.View.addTextNotifier(...).new IModelChangeListener() {...}.modelChange(ModelChangeEvent)

public   void  modelChange(ModelChangeEvent event)  {
  
if (XbeUtilities.VALID_QNAME.equals(event.getPropertyName())) {
    
// might not be in the display thread, need to run in it to
    
// modify the background!
    text.getDisplay().asyncExec(new Runnable() {
      
public void run() {
        
if (cell.getValid())
          text.setBackground(text.getDisplay().getSystemColor(SWT.COLOR_WHITE));
        
else
          text.setBackground(text.getDisplay().getSystemColor(SWT.COLOR_RED));
      }

    }
);
  }

}

可以看到,当触发 modelChange 事件时,单元格会得到通知,因为每个单元格中都添加了 modelChangeListener()。在处理这个事件时,单元格检查它是否有效,并适当地设置对应的文本部件的背景颜色。

结束语

通过使用事件,可以采用简单的编程模型,消除用户界面对模型的依赖性。因此,可以快速创建 Sudoku 游戏应用程序,并避免视图和模型之间的耦合。

可以通过使用 XMLBeans 中的两个扩展点(接口和 PrePostSet)在 XMLBeans 中添加事件功能。使用接口扩展在 XMLBeans 中添加监听器,使用 PrePostSet 扩展捕捉更改并通知相关对象。最后,通过一个配置文件把这些扩展集成到 XMLBeans 生成过程中。

下载

描述名字大小下载方法
XMLBeans 事件和 Sudoku 游戏 Eclipse 项目x-xmlbeanse/SudokuProject.zip2694KBHTTP
用 XML 描述的 Sudoku 游戏板x-xmlbeanse/SudokuBoards.zip4KBHTTP
针对 Windows 编译的 Sudoku 游戏x-xmlbeanse/SudokuApplication-Win32.zip146346KBHTTP


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值