1. 程式人生 > >ini、xml格式配置檔案的解析與拼裝

ini、xml格式配置檔案的解析與拼裝

1.背景

在開發的過程中,我們通常會使用ini、xml、json等配置檔案對某些服務應用的引數進行配置,這些包含各層級結構的配置檔案,大致可以看作樹狀結構,其解析和拼裝並不是一項簡單的事情。

在本專案中,開發人員或者業務人員提供了這些配置檔案之後,需要解析出相應的配置項以及其值,每一項配置都以一條記錄的形式儲存到資料庫中。服務應用以一定的週期對資料庫中的配置項進行讀取並拼裝,以便重新整理本地的配置檔案。

整個業務流程的簡要示意圖如Fig. 1所示:

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所示:

Tab. 1 ini格式檔案配置項在資料庫中的儲存結構

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所示:

Tab. 2 xml格式檔案配置項在資料庫中的儲存結構

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檔案的樹狀結構圖:

Fig. 2 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>型別輸出。

第一部分流程圖如圖所示。

Fig. 3 ini字串轉化成Map<String, Map<String, String>>型別對映表

轉化成一個對映表之後,最後要轉化成List<CItem>型別輸出,其過程如圖Fig. 4所示。

Fig. 4 Map<String, Map<String, String>>型別對映錶轉化成List<CItem>列表

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檔案解析流程如圖所示

Fig. 5 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節點類是有區別的,區別在於:

  1. 前者用於儲存從資料庫裡讀取回來的記錄,因此CItem物件有可能是一個節點,也有是一項屬性;而ConfNode只表示一個節點,該節點的屬性(如果有)保存於其Map<String, String>型別的成員變數property中;
  2. 前者的資料結構中沒有直接體現節點之間父子關係的成員變數,只能通過ConfNodeIndex和ConfNodeName尋找父子關係,而後者ConfNode有List<ConfNode>型別的成員變數Children用於儲存其子節點;
  3. 前者沒有能直接體現當前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個成員變數,接下來先逐一講解。

  1. name:沒啥好說的,就是節點的名字,不過需要注意的是,這個name和ConfNodeName有點不同,例如,Tab. 2中節點的ConfNodeName為Item|Src,那麼對應的name為Src。
  2. nodeVal:節點的值,如果是葉節點,也就是說沒有子節點,那麼就會有非空的節點值。
  3. property:這是一個用於儲存屬性key-value對的對映表,也就是存放例如表Tab. 2中的ID、Title、CorpNo、ShowTopBar及其對應值的對映表。值得一提的是,這裡最好用LinkedHashMap而不是HashMap,因為前者是有序的,前者是後者的一個子類,後者是無序的,如果你希望節點的屬性是按照新增順序輸出的,那麼最好用LinkedHashMap。關於LinkedHashMap和HashMap的詳細用法,可以自行搜尋網上資料。
  4. bIsLeaf:當前節點是否為葉節點標誌,true or false。
  5. 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看一下,這個排列就是我們所需要的順序。具體來講,就是:

  1. 先從ConfNodeIndex來看,當前節點的層級越多(豎槓“|”數量越多),其排序優先順序越低(越往後排),例如Item的ConfNodeIndex是0,Item|Src的是0|0,那麼Item排序顯然高於Item|Src;
  2. 在兩個節點的ConfNodeIndex層級相同的情況下,如果ConfNodeIndex中從左往右數起,豎槓“|”間的數字編號,第一個出現較小的數字者優先順序越高(越往前排),例如Item|Src是0|0,Item|CorpName是0|1,從左往右數,第一個數字都是0,第二個數字起,前者是0,後者是1,那麼前者高於後者;
  3. 至於ConfNodeIndex完全相同者,如果一個是節點(ConfNodeName的豎槓“|”數量與ConfNodeIndex相同),一個是屬性(ConfNodeName的豎槓“|”數量比ConfNodeIndex的多1條),那麼節點高於屬性;
  4. 如果ConfNodeIndex都相同,而且兩者都是屬性,那麼屬性之間的排列誰高誰低可以不用去管,反正這些屬性只是若干組key-value的對映關係而已,新增到ConfNode物件節點中使用的時候是用LinkedHashMap按照新增順序輸出的;
  5. 如果ConfNodeIndex都相同,而且兩者都是節點……那是不可能的事情,不存在的,這樣的話拼裝時就亂了套了,就會撞到一起。

我們按照上述這5項比較規則(實際上只有前3項哈)在這個CItem類中實現了compareLevelByIndex和compareLevelByName這兩個方法,並在重寫compareTo這個方法時,呼叫了這兩個方法,輸入為待比較的CItem物件,輸出為大小比較標誌,1為大於,0為等於,-1為小於。如果不清楚,具體的Comparable介面以及Collections.sort()排序方法可以自行搜尋網上資源進行查詢。

在建立起如圖Fig. 2 所示的樹狀結構之後,我們可以便可以從根節點開始,遞迴地遍歷配置節點,並拼接配置檔案了。

ini格式的檔案是隻有節點沒有屬性的,而xml格式檔案既有節點又可能有屬性,這一點通過觀察Tab. 1和Tab. 2可以看出來。

總結一下,這個類的主要作用有:

  1. 儲存配置項資料;
  2. 讓配置項資料能夠有序地儲存到ConfNode節點中,並構建起ConfNode節點樹或者森林(ConfNode根節點的List)。

這個類的成員函式比較多,但是主要功能都是為了輔助實現上述提到的兩個作用,而且程式碼中包含有註釋,不難理解其含義,這裡不再贅述。(英文註釋是為了防止亂碼,中文註釋是英文編不下去了才寫的…各位看官有實在看不明白之處可以聯絡我…)

這一步實際上就是每一棵配置節點樹建立好了新增到List裡邊就好,這個List就可以看作是一個配置節點森林。在2.3.1.1中建立起一個有序的List<CItem>後,我們利用這個List作為輸入,開始對這個森林進行建立了(輸出為List<ConfNode>)。

對於森林的建立,大體上的思路就是在這個有序的List<CItem>中自上而下地進行遍歷。由於xml格式比ini格式更為複雜,更為通用,我們以xml格式建立森林為例,我們可以對照著Tab. 2進行從上到下搜尋:

  1. 第一個項是Item,索引為0,ConfNodeValue為空,設定為非葉節點,那麼建立對應的ConfNode,並連同其節點名(不包含豎槓“|”,包含豎槓的要去掉,取ConfNodeName最右的名字)新增到一個名為mapStrCNode的Map中,以便後面找到有該節點屬性的時候,將屬性儲存到該節點的成員變數property中;然後由於本節點的ConfNodeIndex和ConfNodeName中都沒有豎槓,因此該節點是根節點,新增到一個用於輸出結果的List<ConfNode>型別的lsCNode中,後續如果再有根節點則繼續新增;
  2. 到第二個項時,Item|ID的索引為0,因此該項是屬性,然後在表中向上尋找其歸屬的節點Item,找到後新增進去;
  3. 到第六個項時,Item|Src的索引為0|0,因此該項是節點,而且ConfNodeValue非空,設定為葉子節點,然後從前一個項的位置向上搜尋其父節點,搜尋到Item了,OK,將本節點新增到Item的子節點列表中,Item節點設定為非葉節點;
  4. 後面的項依次類推。

總結了一下,大致就是,對有序的List<CItem>表進行從高到低的遍歷,遍歷到的節點就建立,然後從前面建立過的節點中尋找到父節點並新增(有序的List,其父節點必然在其前面,而且一般是順著前一項向上找最快,其中原因請結合排序規則進行思考),如果遍歷到了屬性就往前尋找所屬節點,並加入到節點的屬性Map中。值得注意的是,ini格式的檔案中沒有屬性項存在,但是構建森林的程式碼仍然通用。

配置節點建立的過程如圖Fig. 6所示:

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對(對應步驟②)。過程比較簡單,不多贅述。

Fig. 7 ini配置檔案的拼接流程圖

單棵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層以上,而且後者的每一層節點都可能含有屬性。

Fig. 8 xml配置檔案的拼接流程圖

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 單個根節點的xml檔案拼裝

對於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