public class SimpleElement {
private String tagName;
private String text;
private HashMap attributes;
private LinkedList childElements;
public SimpleElement(String tagName) {
this.tagName = tagName;
attributes = new HashMap();
childElements = new LinkedList();
}
public String getTagName() {
return tagName;
}
public void setTagName(String tagName) {
this.tagName = tagName;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public String getAttribute(String name) {
return (String)attributes.get(name);
}
public void setAttribute(String name, String value) {
attributes.put(name, value);
}
public void addChildElement(SimpleElement element) {
childElements.add(element);
}
public Object[] getChildElements() {
return childElements.toArray();
}
}
定义XML语法分析基本元素
为了把一个XML文件处理成为上面提到的简化的DOM树模型,我们必须定义一些基本的语法分析规则。使用这些规则,语法分析程序就能容易地从输入的XML文件中提取标记或者文本块。
第一个是peek,从输入的XML文件中返回下一个字符,而实际上则不必从下层流中获得这个字符。通过保持输入流的完整性,高级函数比如readTag和readText(后面将介绍)可以更加容易地根据它们接下来期待的字符获取需要的内容。
private int peek() throws IOException {
reader.mark(1);
int result = reader.read();
reader.reset();
return result;
} |
下一个方法是skipWhitespce,作用是跳过输入的XML流中的空格、制表符或者回车符。
private void skipWhitespace() throws IOException {
while (Character.isWhitespace((char) peek())) {
reader.read();
}
}
|
在创建了如上所述的这两个方法后,我们就可以写一个函数从输入文件中检索XML标记。
private String readTag() throws IOException {
skipWhitespace();
StringBuffer sb = new StringBuffer();
int next = peek();
if (next != '<') {
throw new IOException
("Expected > but got " + (char) next);
}
sb.append((char)reader.read());
while (peek() != '>') {
sb.append((char)reader.read());
}
sb.append((char)reader.read());
return sb.toString();
}
|
和peek方法联合使用,readTag函数只获得一个标记的内容,而让别的函数去处理其他的内容。
最后的一个方法是readText函数,用来读取XML标记之间的文本。
private String readText() throws IOException {
int[] cdata_start = {'<', '!',
'[', 'C', 'D', 'A', 'T', 'A', '['};
int[] cdata_end = {']', ']', '>'};
StringBuffer sb = new StringBuffer();
int[] next = new int[cdata_start.length];
peek(next);
if (compareIntArrays(next, cdata_start) == true) {
// CDATA
reader.skip(next.length);
int[] buffer = new int[cdata_end.length];
while (true) {
peek(buffer);
if (compareIntArrays
(buffer, cdata_end) == true) {
reader.skip(buffer.length);
break;
} else {
sb.append((char)reader.read());
}
}
} else {
while (peek() != '<') {
sb.append((char)reader.read());
}
}
return sb.toString();
} |
这次使用的peek方法是前面那个从基本的XML文档返回一个字符串序列的peek方法的变体。这个peek变体让语法分析程序判断它将分析的文本是否被装入一个CDATA块。
compareIntArrays函数是一个执行两个整数数组的深度比较的简单程序。
XML语法分析策略和SimpleDOMParser实现
与一个正常的文本文档不同的是,一个符号语法规则的XML文档有一些独特的特点,可以促进语法分析工作:
在一个XML文档中所有的标记都必须匹配。每个起始标记都必须有一个匹配的结束标记,除了当标记本身就是两个起始和结束标记的时候,比如<parser/>就是<parser></parser>的简易格式。标记和属性名称是大小写敏感的。
在一个XML文档中所有的标记都必须正确地嵌套。XML标记不可以交叉嵌套。 例如:一个包含<t1><t2>... </t1></t2>的文档就是错误的,因为结束标记</t1>出现在了结束标记</t2>之前。
着眼于这些规则,SimpleDOMParser语法分析策略就应该遵循如下伪代码所示的模式:
While Not EOF(Input XML Document)
Tag = Next tag from the document
LastOpenTag = Top tag in Stack
If Tag is an open tag
Add Tag as the child of LastOpenTag
Push Tag in Stack
Else
// 结束标记
If Tag is the matching close tag of LastOpenTag
Pop Stack
If Stack is empty
Parse is complete
End If
Else
// 无效标记嵌套
Report error
End If
End If
End While
这算法的关键就是标记堆栈,它可以保存从输入文件中获得的但是没有与它们结束标记匹配的起始标记。堆栈的顶部总是最后的一个起始标记。
除第一个标记以外,每个新的起始标记将是上一个起始标记的子标记。所以语法分析程序把新的标记添加为上一个起始标记的子标记,然后把它推到堆栈的顶部,它就成了最新的起始标记。 另一方面,如果输入标记是一个结束标记,它必须匹配最后一个起始标记。 基于正确嵌套规则,一个不匹配结束标记会出现XML语法错误。 当结束标记匹配最后一个起始标记,语法分析程序从堆栈中弹出最后一个起始标记,因为对于这个标记的分析是完整的。这个处理继续下去,直到堆栈为空为止。此时,你就完成了整个文档的语法分析过程。代码段2给出了SimpleDOMParser.parse方法的整个源代码。
SimpleDOMParser.java
package simpledomparser;
import java.io.Reader;
import java.io.IOException;
import java.io.EOFException;
import java.util.Stack;
public class SimpleDOMParser {
private static final int[] cdata_start = {'<', '!', '[', 'C', 'D', 'A', 'T', 'A', '['};
private static final int[] cdata_end = {']', ']', '>'};
private Reader reader;
private Stack elements;
private SimpleElement currentElement;
public SimpleDOMParser() {
elements = new Stack();
currentElement = null;
}
public SimpleElement parse(Reader reader) throws IOException {
this.reader = reader;
skipPrologs();
while (true) {
int index;
String tagName;
String currentTag = readTag().trim();
if (currentTag.startsWith("</")) {
//结束标记
tagName = currentTag.substring(2, currentTag.length()-1);
//没有起始标记
if (currentElement == null) {
throw new IOException("Got close tag '" + tagName +
"' without open tag.");
}
//结束标记与起始标记不匹配
if (!tagName.equals(currentElement.getTagName())) {
throw new IOException("Expected close tag for '" +
currentElement.getTagName() + "' but got '" +
tagName + "'.");
}
if (elements.empty()) {
//文本处理结束
return currentElement;
} else {
//弹出前面的起始标记
currentElement = (SimpleElement)elements.pop();
}
} else {
// 起始标记或者带有起始标记和结束标记的标记
index = currentTag.indexOf(" ");
if (index < 0) {
// 不带属性的标记
if (currentTag.endsWith("/>")) {
tagName = currentTag.substring(1, currentTag.length()-2);
currentTag = "/>";
} else {
// 起始标记
tagName = currentTag.substring(1, currentTag.length()-1);
currentTag = "";
}
} else {
// 带有属性的标记
tagName = currentTag.substring(1, index);
currentTag = currentTag.substring(index+1);
}
//创建元素
SimpleElement element = new SimpleElement(tagName);
//分析属性
boolean isTagClosed = false;
while (currentTag.length() > 0) {
currentTag = currentTag.trim();
if (currentTag.equals("/>")) {
//结束标记
isTagClosed = true;
break;
} else if (currentTag.equals(">")) {
//起始标记
break;
}
index = currentTag.indexOf("=");
if (index < 0) {
throw new IOException("Invalid attribute for tag '" +
tagName + "'.");
}
// 取得属性名
String attributeName = currentTag.substring(0, index);
currentTag = currentTag.substring(index+1);
// 取得属性值
String attributeValue;
boolean isQuoted = true;
if (currentTag.startsWith("\"")) {
index = currentTag.indexOf('"', 1);
} else if (currentTag.startsWith("'")) {
index = currentTag.indexOf('\'', 1);
} else {
isQuoted = false;
index = currentTag.indexOf(' ');
if (index < 0) {
index = currentTag.indexOf('>');
if (index < 0) {
index = currentTag.indexOf('/');
}
}
}
if (index < 0) {
throw new IOException("Invalid attribute for tag '" +
tagName + "'.");
}
if (isQuoted) {
attributeValue = currentTag.substring(1, index);
} else {
attributeValue = currentTag.substring(0, index);
}
// 添加属性到新的元素中
element.setAttribute(attributeName, attributeValue);
currentTag = currentTag.substring(index+1);
}
// 读取起始标记和结束标记之间的文本
if (!isTagClosed) {
element.setText(readText());
}
// 添加一个新的元素作为目前元素的子元素
if (currentElement != null) {
currentElement.addChildElement(element);
}
if (!isTagClosed) {
if (currentElement != null) {
elements.push(currentElement);
}
currentElement = element;
} else if (currentElement == null) {
// 在文档中只有一个标记
return element;
}
}
}
}
private int peek() throws IOException {
reader.mark(1);
int result = reader.read();
reader.reset();
return result;
}
private void peek(int[] buffer) throws IOException {
reader.mark(buffer.length);
for (int i=0; i<buffer.length; i++) {
buffer[i] = reader.read();
}
reader.reset();
}
private void skipWhitespace() throws IOException {
while (Character.isWhitespace((char)peek())) {
reader.read();
}
}
private void skipProlog() throws IOException {
//跳过"<?" or "<!"
reader.skip(2);
while (true) {
int next = peek();
if (next == '>') {
reader.read();
break;
} else if (next == '<') {
skipProlog();
} else {
reader.read();
}
}
}
private void skipPrologs() throws IOException {
while (true) {
skipWhitespace();
int[] next = new int[2];
peek(next);
if (next[0] != '<') {
throw new IOException("Expected '<' but got '" + (char)next[0] + "'.");
}
if ((next[1] == '?') || (next[1] == '!')) {
skipProlog();
} else {
break;
}
}
}
private String readTag() throws IOException {
skipWhitespace();
StringBuffer sb = new StringBuffer();
int next = peek();
if (next != '<') {
throw new IOException("Expected < but got " + (char)next);
}
sb.append((char)reader.read());
while (peek() != '>') {
sb.append((char)reader.read());
}
sb.append((char)reader.read());
return sb.toString();
}
private String readText() throws IOException {
StringBuffer sb = new StringBuffer();
int[] next = new int[cdata_start.length];
peek(next);
if (compareIntArrays(next, cdata_start) == true) {
// CDATA
reader.skip(next.length);
int[] buffer = new int[cdata_end.length];
while (true) {
peek(buffer);
if (compareIntArrays(buffer, cdata_end) == true) {
reader.skip(buffer.length);
break;
} else {
sb.append((char)reader.read());
}
}
} else {
while (peek() != '<') {
sb.append((char)reader.read());
}
}
return sb.toString();
}
private boolean compareIntArrays(int[] a1, int[] a2) {
if (a1.length != a2.length) {
return false;
}
for (int i=0; i<a1.length; i++) {
if (a1[i] != a2[i]) {
return false;
}
}
return true;
}
}
为了简单起见,SimpleDOMParser不允许在XML文档中使用注解,并且完全忽视XML声明和DOCTYPE。它使用下面的程序来跳过XML声明和DOCTYPE元素。这个程序递归调用它本身,就像处理一个内部DTD一样处理DOCTYPE。
private void skipProlog() throws IOException {
//跳过"<?"或者 "<!"
reader.skip(2);
while (true) {
int next = peek();
if (next == '>') {
reader.read();
break;
} else if (next == '<') {
skipProlog();
} else {
reader.read();
}
}
}
虽然本文中介绍的SimpleDOMParser只有有限的功能,但是对于许多简单的应用程序来说,它仍然是非常有用的。比如说,一个Java applet可以使用它以XML格式传送数据到一个后端服务器应用程序中。因为它是极其轻便的,所以SimpleDOMParser在资源非常有限的环境中更是很有吸引力的。此外,SimpleDOMParser的实现还很简单。虽然目前的实现只能保存元素,而不能保存声明或者DOCTYPE,但是你可以修改它来处理你想要处理的XML文本,而这一切都是非常容易的。 |