ini、xml格式配置檔案的解析與拼裝
1.背景
在開發的過程中,我們通常會使用ini、xml、json等配置檔案對某些服務應用的引數進行配置,這些包含各層級結構的配置檔案,大致可以看作樹狀結構,其解析和拼裝並不是一項簡單的事情。
在本專案中,開發人員或者業務人員提供了這些配置檔案之後,需要解析出相應的配置項以及其值,每一項配置都以一條記錄的形式儲存到資料庫中。服務應用以一定的週期對資料庫中的配置項進行讀取並拼裝,以便重新整理本地的配置檔案。
整個業務流程的簡要示意圖如Fig. 1所示:
本文所涉及的內容,就是後臺管理伺服器對配置檔案的解析,以及應用伺服器對配置檔案的拼裝。
2.實現方案
從Fig. 1的業務流程中可以看出,整個系統需要實現的兩個關鍵功能包括:配置檔案的解析和配置檔案的拼裝。在拼裝的時候我們需要合適的資料結構去承載這些配置項記錄。
2.1配置項的資料結構
在本專案中只包含兩種格式的配置檔案,一種是ini格式,另一種是xml格式。考慮到每一種配置檔案的最小粒度就是一項配置,我們將每一項配置作為一條記錄,存放於資料庫中。
資料庫表的每一條記錄中,關鍵欄位包含配置節點名(或者說配置項名)、配置節點索引、配置節點值,這三項關係到一個配置檔案的解析(拆分)和拼裝,如果缺少其一,配置檔案是無法解析和拼裝的。
當然,這些檔案在資料庫中儲存時所需要的欄位不止這些,例如檔名、服務應用名等,這裡為了簡化模型,突出本文所要描述的技術點,僅給出三個主要欄位。
2.1.1.ini格式的配置檔案
ini的配置檔案格式通常如下所示:
[Section1]
key1=value1
key2=value2
[Section2]
key1=value1
key2=value2
儲存成到資料庫表中對應的結構如下表Tab. 1所示:
ConfNodeName |
ConfNodeValue |
ConfNodeIndex |
Section1 |
0 |
|
Section1|key1 |
value1 |
0|0 |
Section1|key2 |
value2 |
0|1 |
Section2 |
1 |
|
Section2|key1 |
value1 |
1|0 |
Section2|key2 |
value2 |
1|1 |
其中ConfNodeName為完整的配置節點名,ConfNodeValue為節點的值,ConfNodeIndex為該節點在當前配置檔案中的索引(或者說位置)。
比如說Section1是一個父節點,其值為空,其位置是在第1個層級的第0個位置(編號以0開始),那麼ConfNodeIndex為0;在Section1中的節點key1,其ConfNodeName為Section|key1,由於該節點是葉節點(沒有子節點),那麼該節點是有值的,其對應的ConfNodeValue為value1,該節點的位置在第0個節點下的第0個位置,該節點的ConfNodeIndex為0|0;其他節點依次類推。
2.1.2.xml格式的配置檔案
xml的配置檔案格式通常如下所示:
<?xml version="1.0" encoding="utf-8"?>
<Item ID="3.14159" Title="清倉大甩賣" CorpNo="666666" ShowTopBar="true"> <Src>https://www.xxxxxx.com/xxx/wap/enterAction!enter.ac?service=h5_app</Src>
<CorpName>OhYeah</CorpName>
<FuncType>1</FuncType>
<Deskey>test1</Deskey>
<ClientKeyIDUrl AuthName="XCooperation">/OutterStore/OS_GetAuthToken.aspx</ClientKeyIDUrl>
</Item>
儲存成到資料庫表中對應的結構如下表Tab. 2所示:
ConfNodeName |
ConfNodeValue |
ConfNodeIndex |
Item |
0 |
|
Item|ID |
3.14159 |
0 |
Item|Title |
清倉大甩賣 |
0 |
Item|CorpNo |
666666 |
0 |
Item|ShowTopBar |
true |
0 |
Item|Src |
https://www.xxxxxx.com/xxx/wap/enterAction!enter.ac?service=h5_app |
0|0 |
Item|CorpName |
OhYeah |
0|1 |
Item|FuncType |
1 |
0|2 |
Item|Deskey |
test1 |
0|3 |
Item|ClientKeyIDUrl |
/OutterStore/OS_GetAuthToken.aspx |
0|4 |
Item|ClientKeyIDUrl|AuthName |
XCooperation |
0|4 |
對於xml的儲存格式,相對比ini格式複雜,儘管兩者都可以看成是樹狀結構的檔案。xml檔案相對比較複雜的原因主要有:
1). xml檔案中節點的層數可能會存在兩級以上,而ini檔案中節點只有兩級。我們在尋找節點之間的父子關係時,層級越多,難度越大。
2). xml檔案中節點可能會存在屬性項。比如Item節點中就包含了ID、Title、CorpNo、ShowTopBar共4項屬性,這些屬性與所屬的節點處於同一個層級,有著相同的ConfNodeIndex,只是在ConfNodeName中,多了一道豎槓“|”劃分層級,以表示所屬的節點。因此我們在拼裝配置檔案時,從資料庫中讀取了屬於同一個檔案的每條配置項記錄之後,需要區分這條記錄儲存的到底是一個節點,還是一個節點的屬性。區分方法也不難,只要在ConfNodeName中豎槓“|”的數量和ConfNodeIndex中的一樣,該條記錄就是節點;如果ConfNodeName中豎槓“|”的數量比ConfNodeIndex的多1個,那麼該條記錄儲存的就是一項屬性;其他情況?不存在的,只能報錯。
為了更形象生動地描述其樹狀結構,這裡給出這個xml檔案的樹狀結構圖:
2.1.3.用於儲存配置項記錄的類
在與web端進行socket通訊時,我們的報文是json格式的,其中內嵌了配置檔案的資訊。為方便我們進行解析和拼裝,以及對資料庫的儲存,我們建立一個類,每一個配置項儲存為該類的一個物件。
這個類包含了ConfNodeName、ConfNodeValue、ConfNodeIndex三個成員變數,我們只需要使用gson等工具包即可進行請求報文的序列化和反序列化,也就是說,我們可以將儲存著配置項資訊的物件轉換成一個json格式的報文,也可以將json報文中的配置檔案轉換成物件。該配置項類的定義如下:
public class CItem{
private String ConfNodeName;
private String ConfNodeValue;
private String ConfNodeIndex;
public String getConfNodeName(){return this.ConfNodeName;}
public void setConfNodeName(String val){this.ConfNodeName=val;}
public String getConfNodeValue(){return this.ConfNodeValue;}
public void setConfNodeValue(String val){this.ConfNodeValue=val;}
public String getConfNodeIndex(){return this.ConfNodeIndex;}
public void setConfNodeIndex(String val){this.ConfNodeIndex=val;}
}
上面的類中,實際上遠不止這些成員變數,有的東西不便透露,咱只純粹討論解析和拼裝的技術問題,這三個變數夠用了。
2.2.配置檔案的解析
在清楚了配置檔案的資料結構之後,無論是解析還是拼裝配置檔案,都會有比較明確的目標。對配置檔案的解析過程,實質上就是將ini或者xml格式的檔案解析出配置項資料,並將這些配置項資料保存於配置項類CItem中。
2.2.1.ini配置檔案的解析
解析ini配置檔案分成兩個部分,第一部分是將ini格式String轉化成Map<String, Map<String, String>>型別的{Section名}-{key-value對對映表}對映表,可能有點拗口,但是不難理解。第二部分將這個對映表作為輸入,最終轉化成List<CItem>型別輸出。
第一部分流程圖如圖所示。
轉化成一個對映表之後,最後要轉化成List<CItem>型別輸出,其過程如圖Fig. 4所示。
ini檔案解析過程的程式碼實現如下:
public class ParseIniUtil {
public ParseIniUtil(){};
public static Map<String, Map<String, String>> doParse(String strFile) throws Exception {
Map<String,String> mapTemp = null;
StringTokenizer stkFile = new StringTokenizer(strFile, "\r\n");
Map<String, Map<String, String>> mapSection = new LinkedHashMap<String, Map<String, String>>();
while(stkFile.hasMoreTokens()) {
String strLine = stkFile.nextToken().trim();
char ch = strLine.charAt(0);
if(ch != 59 && ch != 35 && ch != 33) {
if(ch == 91) {
String idx = strLine.substring(1, strLine.length() - 1).trim();
mapTemp = new LinkedHashMap<String,String>();
mapSection.put(idx, mapTemp);
} else {
int idx1 = strLine.indexOf("=");
if(idx1 == -1) {
throw new Exception("Ini: no \'=\'");
}
String strKey = strLine.substring(0, idx1);
String strValue = strLine.substring(idx1 + 1);
mapTemp.put(strKey, strValue);
}
}
}
return mapSection;
}
public static List<CItem> parseIni(String strIni) throws Exception {
List<CItem> lsRs = new ArrayList<CItem>();
Map<String, Map<String, String>> mapSection = doParse(strIni);
if ( null != mapSection && 0 < mapSection.size() ) {
Iterator<String> iterParent = mapSection.keySet().iterator();
int iRootIndex = 0;
while (iterParent.hasNext()) {
String strModuleKey = String.valueOf(iterParent.next());
Map<String,String> mapModuleVal = mapSection.get(strModuleKey);
CItem clsRootBp = new CItem();
clsRootBp.setConfNodeName(strModuleKey);
clsRootBp.setConfNodeIndex("" + iRootIndex);
lsRs.add(clsRootBp);
Iterator<String> iterChild = mapModuleVal.keySet().iterator();
int iChildIndex = 0;
while(iterChild.hasNext()){
String strParamKey = String.valueOf(iterChild.next());
String strParamValue = mapModuleVal.get(strParamKey);
CItem clsChildBp = new CItem();
clsChildBp.setConfNodeName(strModuleKey+"|"+strParamKey);
clsChildBp.setConfNodeValue(strParamValue);
clsChildBp.setConfNodeIndex( iRootIndex + "|" + iChildIndex );
lsRs.add(clsChildBp);
iChildIndex++;
}
iRootIndex++;
}
}
return lsRs;
}
}
2.2.2.xml配置檔案的解析
根據配置節點排序的規則(詳情參照3.3.1.1),xml檔案解析流程如圖所示
該過程大致的中心思想就是,(1)順著輸入的xml字串先找<element>頭並新增到輸出結果列表,(2)然後如果該element有屬性就新增屬性到列表,(3)有子節點就遞迴新增子節點,沒有子節點就直接設定當前element的value,(4)找到當前節點的結尾</element>,讓輸入的xml字串等於</element>後面的子串,(4)繼續迴圈,直到xml字串長度為0,或者再也找不到<xxx>字樣的字串,結束迴圈並輸出結果。
對應的程式碼如下:
public class ParseXmlUtil {
public ParseXmlUtil(){}
//遞迴地呼叫,將xml字串解析成一個物件列表
public static List<CItem> xmlStrToList(String strSource, String strParentName, String strParentIndex) {
if(strSource == null) {
return null;
}
List<CItem> lsBp = new ArrayList<CItem>();
int iCurrentNo = 0;
while(strSource.length()>0) {
int iStartPos = strSource.indexOf(60); // 60 '<'
if(iStartPos == -1) {
break;
}
int iEndPos = strSource.indexOf(62, iStartPos); //62 '>'
if(iEndPos == -1) {
break;
}
//如果是<!...>型別,則不將此作為一個元素
char cFirstChar = strSource.charAt(iStartPos + 1);
if(33 == cFirstChar || 63 == cFirstChar) { //33 '!'; 63 '?'
strSource = strSource.substring(iEndPos);
continue;
} else {
String strElemName = strSource.substring(iStartPos + 1, iEndPos).trim();
String strConfNodeName;
String strConfNodeIndex;
int iSpacePos = strElemName.indexOf(" ");
CItem clsBpElement = new CItem();
List<CItem> lsChildBp = new ArrayList<CItem>(); // 用於儲存子串的元素列表
//如果元素包含屬性,那麼元素的名字在第一個空格處結尾
if(-1 != iSpacePos){
String[] arrStrProperty = strElemName.split(" ");
int iArrLen = arrStrProperty.length;
//設定節點名稱以及值
strConfNodeName = (null == strParentName || 0 == strParentName.length())?(arrStrProperty[0]):(strParentName+"|"+arrStrProperty[0]);
strConfNodeIndex = (null == strParentIndex || 0 == strParentIndex.length())? (""+iCurrentNo):(strParentIndex+"|"+iCurrentNo);
clsBpElement.setConfNodeName(strConfNodeName);
clsBpElement.setConfNodeIndex(strConfNodeIndex);
//對於節點是否有子節點,仍需進一步判斷,如果沒有,就設定其值
String strEndTag = "</"+arrStrProperty[0]+">";
int iChildStrEnd = strSource.indexOf(strEndTag);
int iChildStrStart = iEndPos + 1;
//如果沒有子串,或者說子串的長度為0,就既不用設定節點值,也不用遞迴呼叫子串方法
if(iChildStrEnd > iChildStrStart){
String strChildStr = strSource.substring(iChildStrStart, iChildStrEnd).trim();
int iBracketPos = strChildStr.indexOf("<");
//如果節點子串不包含有尖括號,即節點不包含有子節點
if(-1 == iBracketPos){
clsBpElement.setConfNodeValue(strChildStr.trim());
}else{
//遞迴呼叫:當前節點的子串遞迴
lsChildBp = xmlStrToList(strChildStr,strConfNodeName,strConfNodeIndex);
}
}
lsBp.add(clsBpElement);
//設定節點的屬性
for(int i = 1; i<iArrLen; i++) {
int iKeyEndPos = arrStrProperty[i].indexOf("=");
if(-1 == iKeyEndPos){
continue;
}
String strPropName = arrStrProperty[i].substring(0,iKeyEndPos).trim();
int iValStartPos = iKeyEndPos + 1;
//對於屬性,不僅要去除前後的空格,還要去除兩端的引號
String strPropVal = arrStrProperty[i].substring(iValStartPos).trim().replace("\"", "");
//建立屬性並新增到列表
CItem clsBpProp = new CItem();
clsBpProp.setConfNodeName(strConfNodeName+"|"+strPropName);
clsBpProp.setConfNodeIndex(strConfNodeIndex);
clsBpProp.setConfNodeValue(strPropVal);
lsBp.add(clsBpProp);
}
int iEndTagLen = strEndTag.length();
int iEndTagPos = iChildStrEnd;
strElemName = arrStrProperty[0];
//將當前子串截斷到當前節點末尾處
strSource = strSource.substring(iEndTagPos+iEndTagLen).trim();
}else{
//設定節點名稱以及值
strConfNodeName = (null == strParentName || 0 == strParentName.length())?(strElemName):(strParentName+"|"+strElemName);
strConfNodeIndex = (null == strParentIndex || 0 == strParentIndex.length())? (""+iCurrentNo):(strParentIndex+"|"+iCurrentNo);
clsBpElement.setConfNodeName(strConfNodeName);
clsBpElement.setConfNodeIndex(strConfNodeIndex);
//對於節點是否有子節點,仍需進一步判斷,如果沒有,就設定其值
int iChildStrEnd = strSource.indexOf("</"+strElemName+">");
int iChildStrStart = iEndPos + 1;
//如果沒有子串,或者說子串的長度為0,就既不用設定節點值,也不用遞迴呼叫子串方法
if(iChildStrEnd > iChildStrStart){
String strChildStr = strSource.substring(iChildStrStart, iChildStrEnd);
int iBracketPos = strChildStr.indexOf("<");
//如果節點不包含有尖括號,即不包含有子節點
if(-1 == iBracketPos){
clsBpElement.setConfNodeValue(strChildStr.trim());
}else{
//遞迴呼叫:當前節點的子串遞迴
lsChildBp = xmlStrToList(strChildStr,strConfNodeName,strConfNodeIndex);
}
}
lsBp.add(clsBpElement);
}
//將當前節點子串遞迴的返回值進行拼接
if(null != lsChildBp && lsChildBp.size() > 0) {
lsBp.addAll(lsChildBp);
}
//結束條件很重要!!!
String strEndXmlTag = "</" + strElemName + ">";
int iEndTagPos = strSource.indexOf(strEndXmlTag);
if(iEndTagPos == -1) {
break;
}
//lsBp.addAll(xmlStrToList(strSource.substring(iEndTagPos+strEndXmlTag.length()),strConfNodeName,strConfNodeIndex));
//將當前子串截斷到當前節點末尾處
strSource = strSource.substring(iEndTagPos+strEndXmlTag.length()).trim();
}
iCurrentNo++;
}
return lsBp;
}
}
解析和拼裝是互逆的過程,可以在看完拼裝之後回過頭來看解析進行對比,可以加深理解
2.3.配置檔案的拼裝
2.3.1配置節點森林的建立
顧名思義,森林是由多棵樹組成,也就是說一個配置檔案裡面可能存在多個配置根節點,一個根節點就可以表示一棵樹。例如2.1.1中的ini配置檔案,每個Section就是一個根節點,底下的若干個key-value對就是其子節點和子節點的值。
在拼裝配置檔案時,由於配置檔案的配置項節點可以看做是樹狀結構儲存的,如圖Fig. 2所示。為了建立起節點之間的父子關係,咱建立一個節點ConfNode類。這裡必須要注意的是,CItem類和這裡的ConfNode節點類是有區別的,區別在於:
- 前者用於儲存從資料庫裡讀取回來的記錄,因此CItem物件有可能是一個節點,也有是一項屬性;而ConfNode只表示一個節點,該節點的屬性(如果有)保存於其Map<String, String>型別的成員變數property中;
- 前者的資料結構中沒有直接體現節點之間父子關係的成員變數,只能通過ConfNodeIndex和ConfNodeName尋找父子關係,而後者ConfNode有List<ConfNode>型別的成員變數Children用於儲存其子節點;
- 前者沒有能直接體現當前CItem物件是否為葉子節點的成員變數,後者有個布林型的bIsLeaf成員變數用於判斷是否為葉子節點,以便決定是否要拼接其值。
我們為了建立起樹狀結構,首先要把CItem中的內容逐個轉移到ConfNode中,並將ConfNode中的父子關係、是否為葉節點等屬性設定好,最終得到一個或者多個ConfNode根節點,這些根節點組成的List就是一個森林。
這個ConfNode的具體定義如下:
public class ConfNode {
private String name; // 節點名
private String nodeVal = ""; // 節點值
private Map<String, String> property = new LinkedHashMap<String, String>(); // 屬性
private boolean bIsLeaf = true; // 是否葉子節點
private List<ConfNode> lsChildren = new ArrayList<ConfNode>(); // 子節點
public ConfNode(String name) {this.name = name;}
public String getName() {return name;}
public void setName(String name) {this.name = name;}
public String getNodeVal() {return nodeVal;}
public void setNodeVal(String nodeVal) {this.nodeVal = nodeVal;}
public Map<String, String> getProperty() {return property;}
public void setProperty(Map<String, String> property) {this.property = property;}
public boolean isLeaf() {return bIsLeaf;}
public void setIsLeaf(boolean isleaf) {this.bIsLeaf = isleaf;}
public List<ConfNode> getLsChildren() {return lsChildren;}
public void setLsChildren(List<ConfNode> lsChildren) {
this.lsChildren = lsChildren;
if (this.bIsLeaf && this.lsChildren.size() > 0) {
this.bIsLeaf = false;
}
}
/**
* 新增屬性
* @param key
* @param value
*/
public void addProperty(String key, String value) {
this.property.put(key, value);
}
/**
* 新增子節點
* @param el
*/
public void addChild(ConfNode el) {
this.lsChildren.add(el);
if (this.bIsLeaf && this.lsChildren.size() > 0) {
this.bIsLeaf = false;
}
}
}
這個類主要有5個成員變數,接下來先逐一講解。
- name:沒啥好說的,就是節點的名字,不過需要注意的是,這個name和ConfNodeName有點不同,例如,Tab. 2中節點的ConfNodeName為Item|Src,那麼對應的name為Src。
- nodeVal:節點的值,如果是葉節點,也就是說沒有子節點,那麼就會有非空的節點值。
- property:這是一個用於儲存屬性key-value對的對映表,也就是存放例如表Tab. 2中的ID、Title、CorpNo、ShowTopBar及其對應值的對映表。值得一提的是,這裡最好用LinkedHashMap而不是HashMap,因為前者是有序的,前者是後者的一個子類,後者是無序的,如果你希望節點的屬性是按照新增順序輸出的,那麼最好用LinkedHashMap。關於LinkedHashMap和HashMap的詳細用法,可以自行搜尋網上資料。
- bIsLeaf:當前節點是否為葉節點標誌,true or false。
- lsChildren:儲存子節點的List。
實際上,在2.1.3中所提到的CItem類,除了要包含名字、值、索引三個變數以外,還應提供一系列的方法,以便轉存到ConfNode中。其實現應當如下:
public class CItem implements Comparable<CItem>{
private String ConfNodeName; //配置節點名稱
private String ConfNodeValue; // 配置節點項VALUE
private String ConfNodeIndex; // 排序
public String getConfNodeName(){return this.ConfNodeName;}
public void setConfNodeName(String val){this.ConfNodeName=val;}
public String getConfNodeValue(){return this.ConfNodeValue;}
public void setConfNodeValue(String val){this.ConfNodeValue=val;}
public String getConfNodeIndex(){return this.ConfNodeIndex;}
public void setConfNodeIndex(String val){this.ConfNodeIndex=val;}
//To read the first number of index
public int readRootIndex(String strIndex){
int rs = -1;
if(null == strIndex || strIndex.length() <= 0){
return rs;
}
int offset = strIndex.indexOf("|");
if (-1 == offset){
rs = Integer.parseInt(strIndex);
return rs;
}
String str = strIndex.substring(0,offset).trim();
rs = Integer.parseInt(str);
return rs;
}
//Compare the level of the dst to that of the src by index.
//1:higher
//-1:lower
// 0:equal
private int compareLevelByIndex(CItem src, CItem dst){
int rs = 0;
String strSrcIndex = src.getConfNodeIndex();
String strDstIndex = dst.getConfNodeIndex();
while(null != strSrcIndex && null != strDstIndex){
int iSrc = readRootIndex(strSrcIndex);
int iDst = readRootIndex(strDstIndex);
if (iDst > iSrc){
return -1;
}
else if (iDst < iSrc){
return 1;
}
else {
//讀取最高層級
int offsetSrc = strSrcIndex.indexOf("|");
int offsetDst = strDstIndex.indexOf("|");
//如果源沒了,但是目標還有,說明源高階
if(-1 == offsetSrc && -1 != offsetDst){
rs = -1;
return rs;
}
else if(-1 != offsetSrc && -1 == offsetDst){
rs = 1;
return rs;
}
//如果都沒了,說明相等
else if(-1 == offsetSrc && -1 == offsetDst){
rs = 0;
return rs;
}
//否則繼續擷取“|”後邊的子串,以比較下一級
strSrcIndex = strSrcIndex.substring(offsetSrc+1);
strDstIndex = strDstIndex.substring(offsetDst+1);
}
}
return rs;
}
//Compare the level of the dst to that of the src by name.
//1:higher
//-1:lower
// 0:equal
private int compareLevelByName(CItem src, CItem dst){
int rs = 0;
int numOfSrc = 0;
int numOfDst = 0;
String strSrc = src.getConfNodeName();
String strDst = dst.getConfNodeName();
while (null != strSrc){
int iSrc = strSrc.indexOf("|");
if(-1 == iSrc){
break;
}
strSrc = strSrc.substring(iSrc+1);
numOfSrc++;
}
while (null != strDst){
int iDst = strDst.indexOf("|");
if(-1 == iDst){
break;
}
strDst = strDst.substring(iDst+1);
numOfDst++;
}
//The more "|", the lower level.
return numOfDst == numOfSrc ? 0 : (numOfDst > numOfSrc ? -1 : 1);
}
//To get the last offset of pipe symbol |
public int getTailOffPipeSymbol(String str){
//It means input null if return -1 !!
int rsOffset = -1;
int count = 0;
while(null != str){
int len = str.length();
int Offset = str.indexOf("|");
if(-1 == Offset ){
break;
}
rsOffset += Offset;
count++;
if(Offset >= len-1){
break;
}
str = str.substring(Offset+1);
}
rsOffset = rsOffset + count;
return rsOffset;
}
//To get the short name of the node instead of the long one with the whole directory.
public String getShortConfNodeName(){
String strRs = new String(this.getConfNodeName());
int offset = getTailOffPipeSymbol(strRs) + 1;
strRs = strRs.substring(offset);
return strRs;
}
//is property (or node)
public boolean isProperty(){
boolean rs = false;
int numOfSeperatorIndex = 0;
int numOfSeperatorName = 0;
String strIndex = this.getConfNodeIndex();
String strName = this.getConfNodeName();
while (null != strIndex){
int iIndex = strIndex.indexOf("|");
if(-1 == iIndex){
break;
}
strIndex = strIndex.substring(iIndex+1);
numOfSeperatorIndex++;
}
while (null != strName){
int iName = strName.indexOf("|");
if(-1 == iName){
break;
}
strName = strName.substring(iName+1);
numOfSeperatorName++;
}
if(numOfSeperatorName > numOfSeperatorIndex){
rs = true;
}
return rs;
}
//Is current node the property of node src.
public boolean isProperty(CItem src){
boolean rs = false;
if(src.getConfNodeIndex() != this.getConfNodeIndex()){
return rs;
}
String strSrcName = src.getConfNodeName();
String strDstName = this.getConfNodeName();
int offset = strDstName.indexOf(strSrcName);
int srcLen = strDstName.length();
if(0 != offset || srcLen <= strSrcName.length()){
return rs;
}
String strDstShortName = strDstName.substring(srcLen);
if(strDstShortName.indexOf("|") == -1){
rs = true;
}
return rs;
}
//Is current node the child node of Src
public boolean isChild(CItem src){
boolean rs = false;
String strSrcIndex = src.getConfNodeIndex();
String strDstIndex = this.getConfNodeIndex();
if(this.isProperty() || src.isProperty()){
return false;
}
if(strDstIndex.indexOf(strSrcIndex) == -1 || strDstIndex == strSrcIndex){
return false;
}
int offset = strSrcIndex.length() + 1;
String strLastName = strDstIndex.substring(offset);
if(strLastName.indexOf("|") == -1){
rs = true;
}
return rs;
}
//Is current node a root node
public boolean isRoot(){
boolean rs = false;
if(this.isProperty()){
return rs;
}
String strIndex = this.getConfNodeIndex();
if(null == strIndex){
return rs;
}
int offset = strIndex.indexOf("|");
if(-1 == offset){
rs = true;
}
return rs;
}
//Is current node a leaf node
public boolean isLeaf(){
boolean rs = false;
if(this.isProperty()){
return rs;
}
String strVal = this.getConfNodeValue();
if(null != strVal && 0 < strVal.length()){
rs = true;
}
return rs;
}
//Compare the level of the dst to that of the src.
//1:higher
//-1:lower
// 0:equal
@Override
public int compareTo(CItem otherBp) {
int rs = compareLevelByIndex(this,otherBp);
if(0 == rs) {
rs = compareLevelByName(this, otherBp);
}
return rs;
}
}
這個儲存配置項的類實現了Comparable介面,以便在拼裝的時候,通過使用Collections.sort()方法來排序,使輸入引數List<CItem>是有序的。如果無序,拼裝會有難度,至少演算法複雜度會是O(n*n),因為一個節點在尋找其父節點時,每次都會從無序的List中搜索。
既然我們希望List<CItem>有序,那麼這個List到底是按照什麼順序呢?回到Tab. 2看一下,這個排列就是我們所需要的順序。具體來講,就是:
- 先從ConfNodeIndex來看,當前節點的層級越多(豎槓“|”數量越多),其排序優先順序越低(越往後排),例如Item的ConfNodeIndex是0,Item|Src的是0|0,那麼Item排序顯然高於Item|Src;
- 在兩個節點的ConfNodeIndex層級相同的情況下,如果ConfNodeIndex中從左往右數起,豎槓“|”間的數字編號,第一個出現較小的數字者優先順序越高(越往前排),例如Item|Src是0|0,Item|CorpName是0|1,從左往右數,第一個數字都是0,第二個數字起,前者是0,後者是1,那麼前者高於後者;
- 至於ConfNodeIndex完全相同者,如果一個是節點(ConfNodeName的豎槓“|”數量與ConfNodeIndex相同),一個是屬性(ConfNodeName的豎槓“|”數量比ConfNodeIndex的多1條),那麼節點高於屬性;
- 如果ConfNodeIndex都相同,而且兩者都是屬性,那麼屬性之間的排列誰高誰低可以不用去管,反正這些屬性只是若干組key-value的對映關係而已,新增到ConfNode物件節點中使用的時候是用LinkedHashMap按照新增順序輸出的;
- 如果ConfNodeIndex都相同,而且兩者都是節點……那是不可能的事情,不存在的,這樣的話拼裝時就亂了套了,就會撞到一起。
我們按照上述這5項比較規則(實際上只有前3項哈)在這個CItem類中實現了compareLevelByIndex和compareLevelByName這兩個方法,並在重寫compareTo這個方法時,呼叫了這兩個方法,輸入為待比較的CItem物件,輸出為大小比較標誌,1為大於,0為等於,-1為小於。如果不清楚,具體的Comparable介面以及Collections.sort()排序方法可以自行搜尋網上資源進行查詢。
在建立起如圖Fig. 2 所示的樹狀結構之後,我們可以便可以從根節點開始,遞迴地遍歷配置節點,並拼接配置檔案了。
ini格式的檔案是隻有節點沒有屬性的,而xml格式檔案既有節點又可能有屬性,這一點通過觀察Tab. 1和Tab. 2可以看出來。
總結一下,這個類的主要作用有:
- 儲存配置項資料;
- 讓配置項資料能夠有序地儲存到ConfNode節點中,並構建起ConfNode節點樹或者森林(ConfNode根節點的List)。
這個類的成員函式比較多,但是主要功能都是為了輔助實現上述提到的兩個作用,而且程式碼中包含有註釋,不難理解其含義,這裡不再贅述。(英文註釋是為了防止亂碼,中文註釋是英文編不下去了才寫的…各位看官有實在看不明白之處可以聯絡我…)
這一步實際上就是每一棵配置節點樹建立好了新增到List裡邊就好,這個List就可以看作是一個配置節點森林。在2.3.1.1中建立起一個有序的List<CItem>後,我們利用這個List作為輸入,開始對這個森林進行建立了(輸出為List<ConfNode>)。
對於森林的建立,大體上的思路就是在這個有序的List<CItem>中自上而下地進行遍歷。由於xml格式比ini格式更為複雜,更為通用,我們以xml格式建立森林為例,我們可以對照著Tab. 2進行從上到下搜尋:
- 第一個項是Item,索引為0,ConfNodeValue為空,設定為非葉節點,那麼建立對應的ConfNode,並連同其節點名(不包含豎槓“|”,包含豎槓的要去掉,取ConfNodeName最右的名字)新增到一個名為mapStrCNode的Map中,以便後面找到有該節點屬性的時候,將屬性儲存到該節點的成員變數property中;然後由於本節點的ConfNodeIndex和ConfNodeName中都沒有豎槓,因此該節點是根節點,新增到一個用於輸出結果的List<ConfNode>型別的lsCNode中,後續如果再有根節點則繼續新增;
- 到第二個項時,Item|ID的索引為0,因此該項是屬性,然後在表中向上尋找其歸屬的節點Item,找到後新增進去;
- 到第六個項時,Item|Src的索引為0|0,因此該項是節點,而且ConfNodeValue非空,設定為葉子節點,然後從前一個項的位置向上搜尋其父節點,搜尋到Item了,OK,將本節點新增到Item的子節點列表中,Item節點設定為非葉節點;
- 後面的項依次類推。
總結了一下,大致就是,對有序的List<CItem>表進行從高到低的遍歷,遍歷到的節點就建立,然後從前面建立過的節點中尋找到父節點並新增(有序的List,其父節點必然在其前面,而且一般是順著前一項向上找最快,其中原因請結合排序規則進行思考),如果遍歷到了屬性就往前尋找所屬節點,並加入到節點的屬性Map中。值得注意的是,ini格式的檔案中沒有屬性項存在,但是構建森林的程式碼仍然通用。
配置節點建立的過程如圖Fig. 6所示:
配置節點森林的建立,是單獨定義了一個類,然後在類裡面實現一個建立節點森林的方法。其程式碼實現如下:
public class TreeUtil {
public static List<ConfNode> BuildConfigNodeForest(List<CItem> lsBp){
List<ConfNode> lsRs = new ArrayList<ConfNode>();
Map<String, ConfNode> mapStrCNode = new HashMap<String, ConfNode>();
int lsBpSize = lsBp.size();
for(int i = 0; i < lsBpSize; i++){
CItem clsBpHead = lsBp.get(i);
//Is it a Property
if(clsBpHead.isProperty()){
for(int j = i-1; j>=0; j--){
CItem clsBpTemp = lsBp.get(j);
//First found non-property node, add the current prop to it.
if(clsBpHead.isProperty(clsBpTemp)){
ConfNode confNode;
String strName = clsBpTemp.getShortConfNodeName();
confNode = mapStrCNode.get(strName);
String strKey = clsBpHead.getShortConfNodeName();
String strVal = clsBpHead.getConfNodeValue();
confNode.addProperty(strKey, strVal);
//Adding prop finished
break;
}
}
}else{
//else it is a node,
ConfNode clsCNode;
String strName = clsBpHead.getShortConfNodeName();
clsCNode = new ConfNode(strName);
mapStrCNode.put(strName, clsCNode);
//Is it a Leaf Node
if(clsBpHead.isLeaf()){
clsCNode.setIsLeaf(true);
clsCNode.setNodeVal(clsBpHead.getConfNodeValue());
}else{
clsCNode.setIsLeaf(false);
}
//Is it a Root Node
if(clsBpHead.isRoot()){
lsRs.add(clsCNode);
continue;
}
// if it is a child node, we have to find its parent
for(int j = i-1; j>=0; j--){
CItem clsBpParent = lsBp.get(j);
//First found its parent node, add the current child to it.
if(clsBpHead.isChild(clsBpParent)){
//Parent node found
ConfNode parentNode;
String strParentName = clsBpParent.getShortConfNodeName();
parentNode = mapStrCNode.get(strParentName);
parentNode.setIsLeaf(false);
//Is it a Root Node
if(clsBpParent.isRoot() && !lsRs.contains(parentNode)){
lsRs.add(parentNode);
}
ConfNode childNode = clsCNode;
parentNode.addChild(childNode);
break;
}
}
//End for
}
}
return lsRs;
}
}
我在這程式碼裡面已經加上了應有的註釋,結合流程圖Fig. 6以及前面所提到的規則,只要有耐心去慢慢閱讀,應該不難理解其意思。
2.3.2.ini配置檔案的拼裝
對於節點森林的構建,我們已經在前面的步驟中完成了,接下來就是從構建好的森林去拼接配置檔案了。對於ini檔案,其結構真的不能再簡單了,簡單得我都不想去費這個篇幅去講了,但是想想,還是寫上吧,或許便於對xml檔案拼裝的理解。
如圖Fig. 7所示,ini拼裝過程相對簡單,構建好節點森林後(對應步驟①),每一棵配置節點樹的層數都是2,也就是說,每一棵樹的拼裝內容只有頭[Section]和其key-value對(對應步驟②)。過程比較簡單,不多贅述。
單棵ini配置節點樹拼接流程對應的程式碼實現如下:
public class IniAssembleUtil {
public static String lt = "[";
public static String rt = "]";
public static String quotes = "\"";
public static String equal = "=";
public static String blank = " ";
public static String nextLine = "\r\n";// 換行
/**
* @category 拼接INI節點資訊
* @param confNode
* @return
*/
public static StringBuffer confNodeToIni(ConfNode confNode) {
StringBuffer result = new StringBuffer();
// 元素開始
result.append(lt).append(confNode.getName()).append(rt);
result.append(nextLine);
for (ConfNode temp : confNode.getLsChildren()) {
result.append(temp.getName());
result.append(equal);
result.append(temp.getNodeVal());
result.append(nextLine);
}
return result;
}
}
所有ini配置節點樹的拼接,實際上就是每一個節點樹的拼接結果依次組合,其程式碼實現如下:
public class IniConverter {
public static String assembleAsIni (List<CItem> lsItem){
String strIni = null;
//Generate a ConfNode root list
List<ConfNode> lsConfNodeRoot;
StringBuffer sbIni = new StringBuffer();
lsConfNodeRoot = TreeUtil.BuildConfigNodeForest(lsItem);
for (ConfNode rootNode : lsConfNodeRoot) {
sbIni.append(IniAssembleUtil.confNodeToIni(rootNode));
}
try{
strIni = new String(sbIni.toString().getBytes(), "UTF-8");
}catch(Exception e){
e.printStackTrace();
}
return strIni;
}
}
下面可以通過對比xml拼接流程來加深整個拼裝思路的理解。
2.3.3.xml配置檔案的拼裝
xml檔案的拼裝流程如Fig. 8所示,該流程是通過遍歷根節點陣列,對每個根節點依次進行遞迴遍歷,最終得到有序的xml字串。
Fig. 7中的步驟①和Fig. 8中的步驟①完全一樣,不同之處在於步驟②,前者的配置節點樹只有2層,而後者可能會有2層以上,而且後者的每一層節點都可能含有屬性。
xml配置檔案拼接流程的程式碼實現如下:
public class XmlConverter {
public XmlConverter(){}
public static String assembleAsXml (List<CItem> lsBp) throws Exception{
if(null == lsBp || lsBp.size() == 0){
return null;
}
//對物件列表進行排序
Collections.sort(lsBp);
//開始進行節點樹的構建
//Generate a ConfNode root list
List<ConfNode> lsConfNodeRoot;
String strXml = null;
StringBuffer sbXml = new StringBuffer();
lsConfNodeRoot = TreeUtil.BuildConfigNodeForest(lsBp);
for (ConfNode rootNode : lsConfNodeRoot) {
//sbXml.append(XmlAssembleUtil.confNodeToXml(rootNode)).append("\r\n"); //無縮排版
sbXml.append(XmlAssembleUtil.confNode2IndentXml(rootNode,0)).append("\r\n"); //帶縮排版
}
try{
strXml = new String(sbXml.toString().getBytes(), "UTF-8");
}catch(Exception e){
e.printStackTrace();
}
return strXml;
}
}
程式碼中的assembleAsXml方法就是xml檔案拼裝的實現。
在Fig. 8中,步驟①建立配置節點森林的過程已經在2.3.1.2介紹了,接下來主要介紹其中的步驟②,遞迴地遍歷每一棵配置節點樹。該遍歷過程實際上就是按順序遍歷並拼接字串,詳細過程如圖Fig. 9所示。
對於Fig. 9的拼裝流程,總體上可以分為3個部分,即:
①<nodeName key=“value”>
② ……
③</nodeName>
其中最關鍵的部分就是第②部分,這一步如果有子節點,則通過遞迴呼叫的方式,來實現多個層級子節點內容的拼裝;否則直接拼接當前節點的字串內容。這個流程圖就是confNodeToXml方法的實現過程,請結合2.3.1.1中所定義的ConfNode類節點資料結構,來理解confNodeToXml方法的程式碼:
public class XmlAssembleUtil {
public static String lt = "<";
public static String ltEnd = "</";
public static String rt = ">";
public static String rtEnd = "/>";
public static String quotes = "\"";
public static String equal = "=";
public static String blank = " ";
/**
* @category 拼接XML各節點資訊
* @param confNode
* @return
*/
public static StringBuffer confNodeToXml(ConfNode confNode) {
StringBuffer result = new StringBuffer();
// 元素開始
result.append(lt).append(confNode.getName());
// 判斷是否有屬性
if (confNode.getProperty() != null && confNode.getProperty().size() > 0) {
Iterator<String> iter = confNode.getProperty().keySet().iterator();
while (iter.hasNext()) {
String key = String.valueOf(iter.next());
String value = confNode.getProperty().get(key);
result.appen