我们在使用 SAX 对 XML 文件做解析时,往往会被大量的 if 或者 switch 语句所困绕。如果使用适当的设计模式,结合适当的算法,则可以避免在解析程序中到处散布着大量的判断语句。
关于SAX
了解XMLParser的朋友都应该知道,XML有2种基本的解析方式:DOM(DocumentObject Model)、SAX(Simple Api ofXml)。其中,SAX对XML文档的解析不会象DOM那样需要形成树,从而减少了内存的占用,而且SAX是事件触发,用户只需要实现自己关心的接口即可,简化了编程量。
在实际的应用中,如果只是对XML的内容扫描一次,不需要对节点重复操作,则SAX解析方式应该是用户的首选。但是,它也有自己的弱点:
SAX模式是事件触发,用户实现相应的接口方法,对不同节点的判断,主要是根据节点名称。按照常规的处理方式,为了判断当前处理的节点名称,需要使用大量的判断语句。这样,大量的判断语句就散布到解析程序中去了。对于节点的变更,需要修改解析程序中所有接口方法的判断语句,使得整个程序框架难以维护。
组合的设计模式(Composite)
关于设计模式
谈到程序设计,就不能不考虑设计模式。设计模式可以成为我们做设计时相互交流的语言,就象UML是我们建模时统一使用的语言一样。
每一个模式描述了一个在我们做设计时不断重复遇到的问题,以及该问题的解决方案的核心。这样,我们就能一次又一次地使用该方案而不必做重复劳动。其核心在于提供了相关问题的解决方案。
设计模式不能够随意使用。通常我们通过引入额外的间接层次获得灵活性和可变性的同时,也使设计变得更复杂并牺牲了一定的性能。一个设计模式只有当它提供的灵活性是真正需要的时候,才有必要使用。
结构图:
意图:
将对象组合成树形结构以表示"部分-整体"的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。
适用性:
在下列情况下可以考虑使用"组合"模式:
- 你想表示对象的部分-整体层次结构。
- 你希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
我们的使用背景
我们小组计划使用XML作为配置文件,来描述页面的显示,从而能够自动生成相应的JSP代码。下面,我会说明我们的设计思想及其变迁过程,与大家共勉。
在我们的系统里,我们把页面简化后,将页面元素限制在下面若干种。最初,我们的DTD定义如下:
<?xml version="1.0" encoding="gb2312-1"?> <!-- edited with XML Spy v3.0 NT (http://www.xmlspy.com) by liwei (lulusoft) --> <!ELEMENT UserInterface (option+, datashow, control)> <!ELEMENT datashow (maintable?, detailtable, maintable?)> <!--maintable定义主表(表单)的内容--> <!ELEMENT maintable (lable, input+)+> <!--detailtable定义细表(列表)的表头、实际内容;header在生成JSP样式时使用,column在JSP动态生成HTML页面时使用。--> <!ELEMENT detailtable (header, column)+> <!--control包括所有的按钮--> <!ELEMENT control (button+, pagebutton?)> <!--lable的内容就是它的value--> <!ELEMENT lable (#PCDATA)> <!ELEMENT input EMPTY> <!ATTLIST input hidden (true | false) "false" onkeypress CDATA #IMPLIED> <!--checkbox没有属性、没有值--> <!ELEMENT checkbox EMPTY> <!--button的内容就是它的显示值,action属性指定该按钮的连接,onclick属性指定onclick事件的响应--> <!ATTLIST button action CDATA #IMPLIED onclick CDATA #IMPLIED > <!--每个header代表列表表头的一列,只有复选框才允许表头为button(全选/全不选)--> <!ELEMENT header (lable | button)> <!--column定义JSP动态生成HTML页面时该列的内容,每个column可能会包含若干个隐含域,只有第1列可能出现checkbox,并且,checkbox不能有隐含域--> <!ELEMENT button (#PCDATA)> <!ELEMENT column (input+ | checkbox)> <!--pagebutton只是一个标签,定义了一组翻页按钮,包括:上一页、下一页、第几页--> <!ELEMENT pagebutton EMPTY> <!--option定义在当前情况(type)下,每个按钮是否需要输出;每种情况都需要定义一套option--> <!ELEMENT option (type, (enable | disable)+)> <!ELEMENT type (#PCDATA)> <!ELEMENT enable EMPTY> <!ELEMENT disable EMPTY>
在最初的XML解析器中,没有规划对象接口,每个节点的操作针对其特点,各不相同,属性、子元素的存取也没有统一。而且,某些节点没有设计对应的JAVA类,某些JAVA类对应了若干个节点。所以,解析程序中充斥着大量的判断语句。
后来,我们总结了系统需求,发现:有几个基本节点:lable、input、botton,其它的是容器节点,可以包含若干种基本节点。如果想去掉程序中所有的判断语句,就需要对任意节点的操作完全一致。这样,就需要修改我们的设计:
- 扩充DTD,对每个节点增加了classname属性,使得每个节点对象的创建完全一致,通过classname来动态创建;新的DTD见 下载。
- 扩充IxmlElement接口,而且,每个节点都有对应的JAVA类,同时,采用了组合的设计模式(Composite),使得对叶子节点(基本节点)和非叶子节点(容器节点)的处理能够统一。
新的类系图如下:
其次,需要采用新的算法。
- 考虑到SAX是事件触发,我们使用了出入堆栈的方式,基本描述如下:
- 在每个节点元素的开始(startElement接口方法),创建节点对象,并且,保存所有的属性,然后入栈;
- 在每个节点元素的结束(endElement接口方法),从堆栈中弹出2个对象,第1个对象增加到第2个对象中,再把第2个对象入栈;
解析程序的详细代码如下:
package lulusoft.jspbuilderv10.parser; import org.xml.sax.*; import org.xml.sax.helpers.*; import java.io.*; import org.apache.xerces.parsers.SAXParser; import java.util.Stack; import lulusoft.jspbuilderv10.common.*; public class CXmlParser extends org.xml.sax.helpers.DefaultHandler{ /**私有属性*/ CXmlElement root = null; private CharArrayWriter contents = new CharArrayWriter(); private Stack stack = null; /** * 功能描述:构造函数 */ public CXmlParser(){ super(); stack = new Stack(); } /** * 功能描述:标签起始 */ public void startElement( String namespaceURI, String localName, String qName, Attributes attr ) throws SAXException { contents.reset(); String cn = attr.getValue("classname"); if(cn==null || cn.trim().equals("")){ System.out.println("标签未有对应的类!请检查!"); } CXmlElement element = CXmlElement.createElement(cn); int size = attr.getLength() ; for(int i=0;i<size;i++){ String name = attr.getQName(i); String value = attr.getValue(i); if(name.equals("classname")){ continue; } element.addAttribute(name,value); } stack.push(element); } /** * 功能描述:标签结束 */ public void endElement(String namespaceURI, String localName, String qName ) throws SAXException { if(stack.size() > 1){ CXmlElement element1 = (CXmlElement)stack.pop(); CXmlElement element2 = (CXmlElement)stack.pop(); if(contents.toString().trim()!= null){ element1.setElementValue(contents.toString().trim()); } element2.addContainElement(element1); stack.push(element2); } else{ root = (CXmlElement)stack.peek(); } } /** * 功能描述:获得root数据包 */ public CXmlElement getRoot(){ return root; } public void characters( char[] ch, int start, int length ) throws SAXException { contents.write(ch,start,length); } /** * 功能描述:解析xml并返回数据 * @param fileName String xml文件名称 * @return CXmlElement */ public static CXmlElement parseDocument(String fileName){ CXmlParser m_oXmlParser = new CXmlParser(); try { XMLReader xr = new SAXParser(); xr.setContentHandler(m_oXmlParser); xr.parse( new InputSource(new FileReader( fileName )) ); } catch ( SAXParseException saxParseException ) { System.out.println("Parsing Error:"); System.out.println("行:"+saxParseException.getLineNumber()); System.out.println("列:"+saxParseException.getColumnNumber()); System.out.println(" "+saxParseException.getMessage()); } catch(SAXException saxException){ saxException.printStackTrace(); } catch(Exception e){ e.printStackTrace(); } return m_oXmlParser.getRoot(); } }
下载
描述 | 名字 | 大小 |
---|---|---|
实例源代码 | code.zip | 5.8K |
参考资料
学习
- developerWorks 中国网站 XML 专区:在 developerWorks XML 专区可以得到更多技术文章、技巧、教程、标准以及 IBM 红皮书。
- IBM XML 认证:了解如何才能成为一名 IBM 认证的 XML 及相关技术的开发人员。
获得产品和技术
- IBM 试用版软件:使用 IBM 试用版软件构建您的下一个开发项目,这些试用版软件可直接从 developerWorks 下载获得。
讨论
- XML 专区讨论论坛:参与任何与 XML 有关的讨论。
- developerWorks blog:查看这些 blog 并加入 developerWorks 社区。