拿起筆來做刀槍 · 之一 再造一個dom4j

我們的目標並不是做一個大而全的xml解析器,而是,希望能針對性的對特定的(比如spring-style)xml格式進行解析。

所以,根據我們的“從需求出發”原則,我們先看看目標:

解析:

<beans >

	<bean id="searchService" class="cn.com.sitefromscrath.service.SearchServiceInRealBiz">
		<constructor-arg index="1" ref="luceneDAO" />		
		<constructor-arg index="2" ref="mysqlDAO" />
	</bean>
	
	<bean id="luceneDAO" class="cn.com.sitefromscrath.dao.LuceneDAOMock" />
		
	<bean id="mysqlDAO" class="cn.com.sitefromscrath.dao.MysqlDAOMock" />
	
	<bean id="testlevel_one" class="cn.com.sitefromscrath.none1" >
		<bean id="testlevel_two" class="cn.com.sitefromscrath.none2" />
	</bean>

</beans>

並構造一個節點樹結構,供BeanFactory使用。

根據這個xml格式,我們可以看到:

一個節點有:

名稱 如 bean 一個

屬性 key : value 名值對 多個

子節點 多個

因此,我們定義如下的數據結構:

package net.csdn.blog.deltatang.dom4me;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Node {
	
	public String name = null;
	
	protected Map<String, String> attributes = new HashMap<String, String>();
	
	protected List<Node> children = new ArrayList<Node>();	
	
	public String toString(){		
		return "{attr : " + this.attributes.toString() + ", children : " + children.size() + "}";
	 }

	public String getName() {
		return name;
	}

	public Map<String, String> getAttributes() {
		return attributes;
	}

	public List<Node> getChildren() {
		return children;
	}	
		
}

接着,我們繼續寫解析器,由於我們附加了很多讓問題簡單化的約束條件,所以解析並不複雜。

由於此文基於”儘量少依賴“的論述方式,所以我既不會使用正則,也不會使用antlr,

而是簡單的讀取一個個字符進行實時處理,採取”從左往右讀取,從右往左匹配,前溯一個字符“的方式進行解析。

所以,從觀察得到:

節點的開始是: < + char

結束是: /> 或者 </

由於存在嵌套的可能,因此,我們要使用一個 stack堆棧, 對節點進行 入棧 和 出棧 處理。否則,按照上述開始/結束的判斷邏輯,就會將嵌套的內容提前結束,如:

	<bean id="searchService" class="cn.com.sitefromscrath.service.SearchServiceInRealBiz">
		<constructor-arg index="1" ref="luceneDAO" />

實現代碼如下:

package net.csdn.blog.deltatang.dom4me;

import java.io.InputStream;
import java.util.Stack;

public class XmlParser {
	
	DomTree build(InputStream is) {		
		DomTree dt = new DomTree();
		Node root = null;
		
		int b = -1;
		try {
			
			Stack<Node> ns = new Stack<Node>();
			AttrParser ap = null;
						
			Node n = null;
			char prevc = 0;
			
			boolean isblank = false; 
			
			while((b = is.read()) >= 0) {
				char c = (char) b;
				
				isblank = (c == ' ' || c == '\t' || c == '\n');
				
				//node start
				if(prevc == '<' && !isblank && c != '/') {
					
					n = new Node();
					if(!ns.isEmpty()) {
						ns.peek().children.add(n);
					}
					ns.push(n);
					
					ap = new AttrParser(n);
				}
				
				//node end   /> or </
				if(						
						(prevc == '/' && c == '>') 
						||
						(prevc == '<' && c == '/')
						) {		
					
					n = ns.pop();
					if(ns.isEmpty()) {
						root = n;
						break;
					}
					
					ap = null;
				} 

				if(ap != null) {
					ap.read(c);
				}
				
				prevc = isblank ? prevc : c;
				
			}	
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		dt.root = root;
		return dt;		
	}

}

在上述代碼中,出現了一個新類 AttrParser,這個類的功能是用於解析 節點的 屬性 即一系列的名值對。

新建一個類的目的,是爲了將問題簡化成兩個部分:

識別節點和節點的嵌套關係——xmlparser

解析並獲得單個節點的名值對內容——attrparser

代碼如下:

package net.csdn.blog.deltatang.dom4me;

public class AttrParser {
	
	private Node node;
	
	private StringBuffer name;
	private StringBuffer key;
	private StringBuffer val;
	
	// 0 start 1 name 2 key 3 val start 4 val end & new key-val
	int stat = 0;

	public AttrParser(Node node) {
		super();
		this.node = node;
	}
	
	public void read(char c) {
		boolean isblank = (c == ' ' || c == '\t' || c == '\n');
		switch(stat) {
			case 0 : {				
				if(!isblank) {
					name = new StringBuffer();
					name.append(c);
					stat = 1;
				}
				break;
			}
			case 1 : {
				if(!isblank) name.append(c);
				else {
					//name end
					String n = this.name.toString();
					node.name = n;
					name = null;
					
					key = new StringBuffer();
					stat = 2;
				}
				break;
			}
			case 2 : {
				if(c != '=') {
					if(!isblank) key.append(c);
				}else {
					val = new StringBuffer();
					stat = 3;
				}
				break;
			}
			case 3 : {
				if(c == '"') {
					stat = 4;
				}
				break;
			}
			case 4 : {
				if(c == '"') {
					String k = this.key.toString();
					String v = this.val.toString();
					
					node.attributes.put(k, v);					
					key = null;
					val = null;
					
					key = new StringBuffer();
					stat = 2;
					
				} else {
					val.append(c);
				}
				break;
			}
		}
	}

}

注意,因爲我們採取的是順序讀取,因此我們這裏使用了一個狀態機,用以記錄和切換解析的步驟狀態。


最後,我們將解析出來的節點樹命名爲 DomTree 並提供根據ID查詢節點內容的方法:

package net.csdn.blog.deltatang.dom4me;

public class DomTree {

	protected Node root = null;
	
	public Node getById(String id) {
		return _getById(id, root);
	}
	
	private Node _getById(String id, Node node) {
		if(node == null) {
			return null;
		}
		if(id.equals(node.attributes.get("id"))) {
			return node;
		}		
		if(node.children.isEmpty()) {
			return null;
		}
		Node r = null;
		for(int i = 0; i < node.children.size(); i++) {
			Node c = (Node)node.children.get(i);
			 r = _getById(id, c);
			 if(r != null) return r;
		}
		return null;
	}
	
	public String toString(){
		if(root == null) {
			return "EMTPY TREE!";
		}
		
		StringBuilder sb = new StringBuilder();
		getContent(root, 0, sb);
		return sb.toString();
	}

	private void getContent(Node n, int idx, StringBuilder sb) {

		for (int i = 0; i < idx; i++) {
			sb.append('\t');
		}
		sb.append(n).append("\n");

		for (Node nc : n.children) {
			getContent(nc, idx + 1, sb);
		}
	}

}

代碼至此完畢,讓我們測試一下:

	public static void main(String[] args) throws Exception {
		
		XmlParser xmlParser = new XmlParser();
		InputStream is = xmlParser.getClass().getResourceAsStream("./test.xml");
		DomTree dt = xmlParser.build(is);
		is.close();
		
		System.out.println(dt);
		
		System.out.println("search : " + dt.getById("searchService"));
		System.out.println("search : " + dt.getById("mysqlDAO"));
		System.out.println("search : " + dt.getById("testlevel_one"));
		System.out.println("search : " + dt.getById("testlevel_two"));
	}

輸出:

{attr : {}, children : 4}
	{attr : {id=searchService, class=cn.com.sitefromscrath.service.SearchServiceInRealBiz}, children : 2}
		{attr : {ref=luceneDAO, index=1}, children : 0}
		{attr : {ref=mysqlDAO, index=2}, children : 0}
	{attr : {id=luceneDAO, class=cn.com.sitefromscrath.dao.LuceneDAOMock}, children : 0}
	{attr : {id=mysqlDAO, class=cn.com.sitefromscrath.dao.MysqlDAOMock}, children : 0}
	{attr : {id=testlevel_one, class=cn.com.sitefromscrath.none1}, children : 1}
		{attr : {id=testlevel_two, class=cn.com.sitefromscrath.none2}, children : 0}

search : {attr : {id=searchService, class=cn.com.sitefromscrath.service.SearchServiceInRealBiz}, children : 2}
search : {attr : {id=mysqlDAO, class=cn.com.sitefromscrath.dao.MysqlDAOMock}, children : 0}
search : {attr : {id=testlevel_one, class=cn.com.sitefromscrath.none1}, children : 1}
search : {attr : {id=testlevel_two, class=cn.com.sitefromscrath.none2}, children : 0}

結果符合預期。
發佈了94 篇原創文章 · 獲贊 6 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章