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对就是其子节点和子节点的值。

2.3.1.1.节点的数据结构

在拼装配置文件时,由于配置文件的配置项节点可以看做是树状结构存储的,如图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)。

这个类的成员函数比较多,但是主要功能都是为了辅助实现上述提到的两个作用,而且代码中包含有注释,不难理解其含义,这里不再赘述。(英文注释是为了防止乱码,中文注释是英文编不下去了才写的…各位看官有实在看不明白之处可以联系我…)

2.3.1.2.建立配置节点森林

这一步实际上就是每一棵配置节点树建立好了添加到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.append(blank).append(key).append(equal).append(quotes)

                        .append(value).append(quotes);

            }

        }

        result.append(rt);// 结束标记

      /*

       * 判断是否是叶子节点 是叶子节点,添加节点内容 不是叶子节点,循环添加子节点

       */

        if (confNode.isLeaf()) {

            result.append(confNode.getNodeVal());

        } else {

            for (ConfNode temp : confNode.getLsChildren()) {

                result.append(confNodeToXml(temp));

            }

        }

        // 节点结束

        result.append(ltEnd).append(confNode.getName()).append(rt);

        return result;

    }



    /**

     * @category 拼接XML申明信息

     * @param confNode

     * @return

     */

    public static String confNode2Xml(ConfNode confNode) {



        StringBuffer body = confNodeToXml(confNode);

        StringBuffer head = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n");

        head.append(body);

        return head.toString();

    }

}

至此,xml文件的拼装流程告一段落。

3.后记

3.1.总结

ini、xml配置文件的解析和拼装,实际上是对这些配置文件树结构的拆分和构建,充分利用递归的方式可以节省代码量。在对每一项记录进行遍历之前,对List进行排序可以减少后续的时间复杂度,磨刀不误砍柴工。

3.2.代码下载及使用

3.2.1.代码下载链接

拼装和解析的代码下载链接:

https://gitee.com/vincent_yu/ConfigFile/

2.3.2代码目录结构以及使用方式

整个代码的目录结构如图所示。

Fig. 10 代码目录结构

util包下是对应配置文件解析和拼装的实现代码。

treeutil包是负责将CItem类型记录项的列表转化成有序的ConfNode节点森林的代码。

iniutil负责ini配置文件的解析和拼装,ParseIniUtil负责解析ini文件成CItem对象列表,IniAssembleUtil负责单棵配置节点树的拼装,IniConvert调用单棵配置节点树拼装的方法实现所有树的拼接。

xmlutil负责xml配置文件的解析和拼装,ParseXmlUtil负责解析xml文件成CItem对象列表,XmlAssembleUtil负责单棵配置节点树的拼装,XmlConvert调用单棵配置节点树拼装的方法实现所有树的拼接。

com.vin下的TestDemo是测试配置文件解析和拼装的主程序入口类。里面的注释将ini的解析、拼装和xml的解析、拼装测试代码分成4个部分,测试哪一部分就留下哪一部分,其他部分注释掉。

 

公众号二维码.jpg

欢迎关注!

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值