为了让应用程序有效地处理XML数据,你必须向SAX解析器注册处理程序。处理程序也称Handler接口,是由SAX定义的一组回调方法组成的,这些方法使你可以在相关的事件发生时对其进行编程。
在SAX2.0中定义了四大核心接口:org.xml.sax.ContentHandler,org.xml.sax.ErrorHandler,org.xml.sax.DTDHandler以及org.xml.sax.EntityResolver。
其于SAX的XML应用程序必须实现一个或多个Handler接口,并为Handler接口中的回调方法编程(如果你不想添加代码,也可以不写,这样就可以忽略这种类型的事件)。然后使用XMLReader中的setContentHandler(),setErrorHandler(),setDTDHandler()和setEntityResolver()方法进行注册即可。在解析过程中,reader会在合适的处理类中调用这些回调方法。
我们将从实现ContentHandler接口开始讲解,ContentHandler,顾名思义,表示的是与XML文档内容处理有关的事件,如元素,属性,字符数据等。示例:
package xml; import java.io.File; import java.io.FileInputStream; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; public class ParseXML1 { public static void main(String[] args) { try { XMLReader reader = XMLReaderFactory.createXMLReader(); reader.setContentHandler(new MyContentHandler()); //注册 reader.parse(new InputSource(new FileInputStream( new File("/home/fuhd/apk/gw/com.application.zomato.apk/AndroidManifest.xml")))); } catch (SAXException e1) { e1.printStackTrace(); } catch(Exception e2){ e2.printStackTrace(); } } } //Handler接口实现,这里没有实现什么内容,稍后实现 class MyContentHandler implements ContentHandler { @Override public void setDocumentLocator(Locator locator) { // TODO Auto-generated method stub } @Override public void startDocument() throws SAXException { // TODO Auto-generated method stub } @Override public void endDocument() throws SAXException { // TODO Auto-generated method stub } @Override public void startPrefixMapping(String prefix, String uri) throws SAXException { // TODO Auto-generated method stub } @Override public void endPrefixMapping(String prefix) throws SAXException { // TODO Auto-generated method stub } @Override public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { // TODO Auto-generated method stub } @Override public void endElement(String uri, String localName, String qName) throws SAXException { // TODO Auto-generated method stub } @Override public void characters(char[] ch, int start, int length) throws SAXException { // TODO Auto-generated method stub } @Override public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { // TODO Auto-generated method stub } @Override public void processingInstruction(String target, String data) throws SAXException { // TODO Auto-generated method stub } @Override public void skippedEntity(String name) throws SAXException { // TODO Auto-generated method stub } }
文档定位器
第一个需要实现的回调方法是setDocumentLocator(),它用于设置在其他SAX事件中需要使用的org.xml.sax.Locator对象。当某个回调事件被触发时,实现了SAX解析器处理接口的类通常需要在XML文件中找到相应的位置。Locator类有几个很有用的方法,例如getLineNumber()和getColumnNumber(),它们可以返回调用时正在解析的XML文档的位置。例:
class MyContentHandler implements ContentHandler { private Locator locator; @Override public void setDocumentLocator(Locator locator) { this.locator = locator; } //...........其它回调方法............... }
警告:Locator实例只能在ContentHandler实现的作用域范围内使用,在解析过程外,使用Locator对象,其结果是难以预料的(也是无用的)。
文档解析的开始和结束
任何处理过程都有开始和结束。这两个重要的事件都只会发生一次:前者在所有其他解析事件发生之前发生,而后者则是在所有解析事件之后发生。SAX提供了回调方法:startDocument()和endDocument()来表示这些事件。
startDocument()方法在所有其他解析事件的回调方法之前被调用,这样就保证了解析有一个明确的起始点。endDocument()在所有处理类中总是最后被调用的方法,即使在发生错误从而导致解析被终止的情况下也是如此。注意:若有不可恢复的错误发生,则ErrorHandler类的回调方法就会被调用,最后再调用endDocument()方法结束解析过程。示例代码:
@Override public void startDocument() throws SAXException { // TODO Auto-generated method stub } @Override public void endDocument() throws SAXException { // TODO Auto-generated method stub }
示例代码中,无需对这些方法进行任何处理,但因为实现了ContentHandler接口,因而仍给出方法的实现。
处理指令
在XML中处理指令比较特殊。它们并非XML的元素,而是由调用程序来进行不同的处理。由于这些特征,SAX定义了特殊的回调方法来处理指令。processingInstruction()这个方法接收处理指令的目标和发送给处理指令的所有数据。
@Override public void processingInstruction(String target, String data) throws SAXException { // TODO Auto-generated method stub }
名称空间回调方法
除XML模式(Schema)外,XML名称空间是自最早的XML 1.0推荐标准以来,XML中增加的最为重要的概念。在SAX2.0中,从元素层面就支持名称空间。这样可以将元素前缀和相关名称空间URI表示的元素名称空间,和一个元素的本地名区分开来。本地名(Local name)指的是没有前缀的元素名。例如,元素rdf:li,其中li就是本地名,rdf是名称空间的前缀,其名称空间的URI可能被声明为:http://www.w3.org/1999/02/22-rdf-syntax-ns#。
有两个SAX回调方法专门用于处理名称空间。当解析器到达前缀映射(Prefix Mapping)的开头和结尾时,它们将被调用。虽然前缀映射是一个新术语,但并非新概念,它就是一个用xmlns属性来声明名称空间的元素。虽然它通常是在根元素中使用,但在XML文档中的任何元素都能声明明确的名称空间。例如:
<catalog> <books> <book title="XML in aNutshell" xmlns:xlink="http://www.w3.org/1999/xlink"> <cover xlink:type="simple" xlink:show="onLoad" xlink:href="xmlnutCover.jpg" ALT = "XML in a Nutshell" width="125" height="350" /> </book> </books> </catalog>
在本例中,文档中声明了一个明确的名称空间,内置了几个元素。这样元素和元素中的属性都可以获得前缀和URI映射(分别是xlink和http://www.w3.org/1999/xlink)。
startPrefixMapping()回调方法的参数是名称空间前缀和与其相关联的URI。当声明映射的元素结束时,这时映射被称为是“关闭的”或“结束的”,这将触发endPrefixMapping()方法。回调方法唯一的问题是不能以连续方式操作,而SAX通常都是按照这种方式构建的。前缀映射回调恰在声明名称空间的元素回调之前发生,且映射的结束将恰在声明元素完成后导致一个事件发生。如下例:
...... /**将URI保存在前缀映射*/ private Map namespaceMappings; public JTreeHandler(DefaultTreeModel treeModel,DefaultMutableTreeNode base) { this.treeModel= treeModel; this.current = base; this.namespaceMappings = new HashMap(); } public void startPrefixMapping(String prefix,String uri) { namespaceMappings.put(uri,prefix); } public void endPrefixMapping(String prefix) { for(Iterator i = namespaceMappings.keySet().iterator();i.hasNext();){ String uri = (String)i.next(); String thisPrefix = (String) namespaceMappings.get(uri); if(prefix.equals(thisPrefix)){ namespaceMappings.remove(uri); break; } } }
见上例,在startPrefixMaping()中添加所报告的前缀和URI,然后在endPrefixMapping()中删除。如果你不保存通过startPrefixMapping()报告的这些前缀,它们在元素的回调代码中就不会出现。最简单的保存方式是使用Map。
注意:我们使用URI而不是前缀作为映射的关键字,因为startElement()方法会报告元素的名称空间URI,而不是前缀,因此以URI作为关键字可以使查询更快。然后,在endPrefixMapping()中可以看到,当映射不可用的时候,在删除映射时要增加一些工作。
元素回调方法
与获取数据有关的三个主要事件是:元素的开始,结束和character()回调方法。它们告诉我们,已经解析了元素的开始标签,并且找到了元素包含的数据,以及何时执行到元素结束标签。
其中第一个回调方法startElement()为程序提供XML元素及其属性的信息。这个回调方法的参数为元素的名称(有多种形式)和一个org.xml.sax.Attributes实例。Attribute接口(或者说,解析器对其的实现)保存着元素中所有属性的引用。它可以以类似Vector的形式简单地遍历元素的属性。除了能够按索引引用属性(当遍历所有属性时使用)外,也可以使用名字来引用属性。当然,当我们看到表示XML元素或属性的“名字”这个词时必须要小心,因为它可以指代很多东西。在这种情况下,我们可以使用属性的QName,也可以在用到了名称空间时,使用本地名和名称空间URI的组合。还有一些辅助方法可以给属性加上名称空间前缀信息,例如getURI(int index)和getLocalName(int index)。总体上来讲,属性接口为元素属性提供了丰富的信息。
除元素属性之外,我们还获得了多种形式的元素名字。这仍然要考虑到XML名称空间。首先提供的是元素的名称空间URI。这样就把元素放到了文档的名称空间完整集合的合适的上下文中,即前面提及的无前缀元素名字。另外也提供了QName。
@Override public void startElement(String uri, String localName, String qName, Attributes atts) throws SAXException { // TODO Auto-generated method stub } @Override public void endElement(String uri, String localName, String qName) throws SAXException { // TODO Auto-generated method stub }
元素数据
标识好程序中元素块的开始和结束,列举了元素属性后,下一个重要的信息是包括在元素自身之中的实际数据。它通常由另外一些元素,文本数据或是两者结合组成。当出现其他元素时,将初始化这些元素的回调方法同时进行一种伪递归(pseudorecursion)操作:元素之间的嵌套会导致回调方法之间的“嵌套”。而在某些时候又会碰到文本数据。通常来说对XML客户程序而言,最重要的信息就是这些文本数据了,因为这些数据要么是需要在客户端显示,要么是对客户端的响应而生成的数据。不同解析器的实现方式是不一样的,通常会设计算法来提高解析速度。
在SAX中,元素中的文本数据通过characters()回调方法传递给应用程序,这个方法给应用程序提供了字符数组,以及要读取的字符的起始索引和长度。从这个数组中生成一个字符串,并使用这个数据:
public void characters(char[] ch,int start, int length) throws SAXException { String s = new String(ch, start ,length); //............................. }
注意:characters()回调方法接收一个字符数组,以及起始索引和长度作为参数,去标识从哪个位置开始读取数据,以及读到数组的哪个位置为止。这会产生某些混乱,常见的错误就是像这样读取字符数组中的数据:
public void characters(char[] ch,int start,int length) throws SAXException { for(int i=0; i<ch.length; i++) System.out.println(ch[i]); }
这里的错误在于它是从头到尾的读取字符数组。而不是从指定的起始索引读到指定的长度为止。
另外,characters()方法经常会报告空白,这很容易引起混乱,因为ignorableWhitespace()回调方法也会报告空白。你可以记住一个简单的规则来避免这种混乱:”不使用模式(Schema),就不会调用ignorableWhitespace()方法“。注意:模式表示约束模型,包括DTD,XML Schema,Relax等。如果没有DTD与XML Schema,将不会触发ignorableWhitespace()回调方法,只触发characters()回调方法。我们再深入一步进行探讨,如果元素中只能包含其他元素,事情就变得相当清楚了,元素之间的空白是可以被忽略的。但是,考虑如下这个混合的内容模型:
<!ELEMENT LINE (#PCDATA | STAGEDIR)*>
在这个模型中,无论是否关联到DTD或者Schema,LINE的开始标签和结束标签之间没有可以忽略的空白。那是因为它无法区分这些空白是用来增加可读性还是文档本身就含有的。例如:
<SPEAKER>CELIA</SPEAKER> <LINE> <STAGEDIR>Reads</STAGEDIR> </LINE> <LINE>Why should this a desert be?</LINE>
在这个XHTML程序段中,位于LINE元素和STAGEDIR元素之间的空白是不能被忽略的,要通过characters()回调方法报告出来,提供给予字符有关的回调方法。
可忽略的空白
我们已经深入讨论了空白的相关知识,这样实现ignorableWhitespace()方法显然就不在话下了,因为空白是可被忽略的,所以我们代码直接将它忽略就可以了:
public void ignorableWhitespace(char[] ch,int start ,int length) throws SAXException { //这是可忽略的,因此不需要显示出来 }
实体
实体经常用来引用其他的XML程序段和特殊字符(例如 & 和 >)。当XML文档被解析时,引用其他文档的实体通常会被展开,并将其插入到XML文档中。然而,非验证型的解析器不要求解析实体引用,而是会忽略它; 另外,你也可以设置解析器有意地忽略实体。在这些情况下,当实体被忽略时,SAX就会调用一个回调方法来说明这种情况的发生。回调方法将给出实体名字,如何:
public void skippedEntity(String name) throws SAXException { //忽略 }
大多数情况下,你不会看到这个回调方法被执行,目前绝大部分已有的解析器,即使是非验证型的,都不会忽略实体。例如Apache Xerces解析器就不会调用这个回调方法,相反地,实体引用将会被扩展,并将其结果包含在XML数据当中返回给应用程序。注意:传递给回调方法的参数不要包括实体引用中前面的 & 符号和最后的分号。对于&header;来说,只需要把实体引用的名字header,传递给skippedEntity()就可以了。
结果
最后,我要向已经实例化的XMLReader注册内容处理类。这是通过setContentHandler()方法完成的:
public static void main(String[] args) { try { XMLReader reader = XMLReaderFactory.createXMLReader(); reader.setContentHandler(new MyContentHandler()); //注册 reader.parse(new InputSource(new FileInputStream( new File("/home/fuhd/apk/gw/com.application.zomato.apk/AndroidManifest.xml")))); } catch (SAXException e1) { e1.printStackTrace(); } catch(Exception e2){ e2.printStackTrace(); } } class MyContentHandler implements ContentHandler {//....................}