乐者为王

Do one thing, and do it well.

ANTLR 4权威参考读书笔记(26)

今天准备做的是把JSON文本文件转换成XML文本文件。

把JSON转换成XML

许多Web服务返回的是JSON数据,但是我们可能会遇到一种情况,需要把JSON数据送给那些只接受XML数据的代码。这就需要我们构建一个JSON到XML的转换器。我们的目标是读入像下面这样的JSON数据:

1
2
3
4
5
6
7
{
"description" : "An imaginary server config file",
"logs" : {"level":"verbose", "dir":"/var/log"},
"host" : "antlr.org",
"admin": ["parrt", "tombu"],
"aliases": []
}

放出等价的XML数据,就像下面这样的:

1
2
3
4
5
6
7
8
9
10
11
<description>An imaginary server config file</description>
<logs>
  <level>verbose</level>
  <dir>/var/log</dir>
</logs>
<host>antlr.org</host>
<admin>
  <element>parrt</element>
  <element>tombu</element>
</admin>
<aliases></aliases>

正如我们对CSV做的那样,让我们给JSON语法中的一些选项打上标签,以便让ANTLR生成更精确的监听器方法。

1
2
3
4
5
6
7
8
object
    : '{' pair (',' pair)* '}'    # AnObject
    | '{' '}'                     # EmptyObject
    ;
array
    : '[' value (',' value)* ']'  # ArrayOfValues
    | '[' ']'                     # EmptyArray
    ;

我们将对规则value做同样的事,但是稍有不同。除3个选项外的其它所有选项只需要返回被匹配的值的文本,所以我们可以为其它所有选项使用相同的标签,使语法分析树遍历器为那些选项触发相同的监听器方法。

1
2
3
4
5
6
7
8
9
value
    : STRING    # String
    | NUMBER    # Atom
    | object    # ObjectValue
    | array     # ArrayValue
    | 'true'    # Atom
    | 'false'   # Atom
    | 'null'    # Atom
    ;

为构建这样的转换器,明智的做法是让每个规则返回被它匹配的输入短语的XML等价物。为追踪部分结构,我们使用字段xml和两个帮助方法来注解语法分析树。

1
2
3
4
public static class XMLEmitter extends JSONBaseListener {
    ParseTreeProperty<String> xml = new ParseTreeProperty<String>();
    String getXML(ParseTree ctx) { return xml.get(ctx); }
    void setXML(ParseTree ctx, String s) { xml.put(ctx, s); }

我们把每棵子树转换后的字符串挂载到该子树的根节点。在语法分析树更高节点上工作的方法可以捕获这些值以便计算更大的字符串。然后挂载在根节点上的字符串完成计算。

让我们从最简单的转换开始。value的Atom选项返回匹配记号的文本。

1
2
3
public void exitAtom(JSONParser.AtomContext ctx) {
    setXML(ctx, ctx.getText());
}

字符串基本上是相同的,只是我们必须去除双引号。

1
2
3
public void exitString(JSONParser.StringContext ctx) {
    setXML(ctx, stripQuotes(ctx.getText()));
}

如果value()规则方法找到一个对象或数组,它可以把组合元素的部分转换拷贝到它自己的语法分析树节点。以下代码是找到对象时的处理:

1
2
3
4
public void exitObjectValue(JSONParser.ObjectValueContext ctx) {
    // analogous to String value() {return object();}
    setXML(ctx, getXML(ctx.object()));
}

一旦我们可以转换所有的值,我们需要担心名-值对以及把它们转换成标签和文本。生成的XML的标签名字来源于STRING ':' value选项中的STRING。在左右尖括号之间的文本来源于挂载在value子节点上的文本。

1
2
3
4
5
6
public void exitPair(JSONParser.PairContext ctx) {
    String tag = stripQuotes(ctx.STRING().getText());
    JSONParser.ValueContext vctx = ctx.value();
    String x = String.format("<%s>%s</%s>\n", tag, getXML(vctx), tag);
    setXML(ctx, x);
}

JSON对象由名-值对组成。因此,对于被选项中标记为AnObject的object找到的每个对,我们把计算后的结果追加在语法分析树。

1
2
3
4
5
6
7
8
9
10
11
public void exitAnObject(JSONParser.AnObjectContext ctx) {
    StringBuilder buf = new StringBuilder();
    buf.append("\n");
    for (JSONParser.PairContext pctx : ctx.pair()) {
        buf.append(getXML(pctx));
    }
    setXML(ctx, buf.toString());
}
public void exitEmptyObject(JSONParser.EmptyObjectContext ctx) {
    setXML(ctx, "");
}

处理数组遵循相似的模式,只是简单地连接来自子节点的结果列表,然后把它们包裹在<element>标签中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void exitArrayOfValues(JSONParser.ArrayOfValuesContext ctx) {
    StringBuilder buf = new StringBuilder();
    buf.append("\n");
    for (JSONParser.ValueContext vctx : ctx.value()) {
        buf.append("<element>"); // conjure up element for valid XML
        buf.append(getXML(vctx));
        buf.append("</element>");
        buf.append("\n");
    }
    setXML(ctx, buf.toString());
}
public void exitEmptyArray(JSONParser.EmptyArrayContext ctx) {
    setXML(ctx, "");
}

最后,我们需要使用从一个对象或数组收集来的全部转换注解语法分析树的根节点。

1
2
3
json: object
    | array
    ;

我们可以在监听器里用一个集合运算做到这点。

1
2
3
public void exitJson(JSONParser.JsonContext ctx) {
    setXML(ctx, getXML(ctx.getChild(0)));
}

以下是构建和测试序列:

1
2
3
antlr JSON.g
compile *.java
run JSON2XML t.json

下面的是部分的输出结果:

1
2
3
4
<description>An imaginary server config file</description>
<logs>
<level>verbose</level>
...

有些转换不总是像JSON到XML那样直白的。但是,这个例子向我们表明如何通过拼凑部分翻译短语处理句子转换问题。

Comments