本文的第 1 部分簡(jiǎn)要討論了語(yǔ)法、解析器和 BNF。然后它介紹了 JavaCC,一個(gè)流行的解析器生成器。第 2 部分演示了如何修改第 1 部分中的樣本代碼,這樣就可以使用附加工具 JJTree 來(lái)構(gòu)建相同解析的解析樹(shù)表示。您將探索這種方法的優(yōu)點(diǎn),并研究如何編寫(xiě) Java 代碼在運(yùn)行時(shí)遍歷該解析樹(shù)以便恢復(fù)其狀態(tài)信息,并對(duì)正在解析的表達(dá)式求值。本文結(jié)尾將演示如何開(kāi)發(fā)通用例程,用于遍歷從一小部分 XQuery 語(yǔ)法生成的解析樹(shù),并對(duì)其求值。
使用 JavaCC 解析器生成器有一個(gè)嚴(yán)重缺點(diǎn):許多或大多數(shù)客戶機(jī)端 Java 代碼需要嵌入到 .jj 語(yǔ)法腳本中,該腳本編碼了您的 BNF(巴科斯-諾爾范式,Backus-Naur Form)。這意味著您失去了在開(kāi)發(fā)周期中合適的 Java IDE 可以向您提供的許多優(yōu)點(diǎn)。
開(kāi)始使用 JJTree 吧,它是 JavaCC 的伙伴工具。JJTree 被設(shè)置成提供一個(gè)解析器,該解析器在運(yùn)行時(shí)的主要工作不是執(zhí)行嵌入的 Java 操作,而是構(gòu)建正在解析的表達(dá)式的獨(dú)立解析樹(shù)表示。這樣,您就可以獨(dú)立于生成該解析樹(shù)的解析代碼,捕捉在運(yùn)行時(shí)易于遍歷和查詢的單個(gè)樹(shù)中的解析會(huì)話的狀態(tài)。使用解析樹(shù)表示還會(huì)使調(diào)試變得更容易,并縮短開(kāi)發(fā)時(shí)間。JJTree 是作為 JavaCC 分發(fā)版(distribution)的一部分發(fā)布的(請(qǐng)參閱 參考資料)。
在我們繼續(xù)之前,我要特別提一下,術(shù)語(yǔ) 解析樹(shù)和 抽象語(yǔ)法樹(shù)(或 AST)描述了非常相似的語(yǔ)法結(jié)構(gòu)。嚴(yán)格地講,對(duì)于我在下面提到的解析樹(shù),語(yǔ)言理論家更精確地把它稱作 AST。
要使用 JJTree,您需要能夠:
- 創(chuàng)建 JJTree 作為輸入獲取的 .jjt 腳本
- 編寫(xiě)客戶機(jī)端代碼以遍歷在運(yùn)行時(shí)生成的解析樹(shù)并對(duì)其求值
本文演示了如何執(zhí)行這兩種操作。它并沒(méi)有涵蓋所有內(nèi)容,但肯定能帶您入門(mén)。
JJTree 基礎(chǔ)知識(shí)
JJTree 是一個(gè)預(yù)處理器,為特定 BNF 生成解析器只需要簡(jiǎn)單的兩步:
- 對(duì)所謂的 .jjt 文件運(yùn)行 JJTree;它會(huì)產(chǎn)生一個(gè)中間的 .jj 文件
- 用 JavaCC 編譯該文件( 第 1 部分中討論了這個(gè)過(guò)程)
幸運(yùn)的是,.jjt 文件的結(jié)構(gòu)只是我在第 1 部分中向您顯示的 .jj 格式的較小擴(kuò)展。主要區(qū)別是 JJTree 添加了一個(gè)新的語(yǔ)法 node-constructor構(gòu)造,該構(gòu)造可以讓您指定在解析期間在哪里以及在什么條件下生成解析樹(shù)節(jié)點(diǎn)。換句話說(shuō),該構(gòu)造管理由解析器構(gòu)造的解析樹(shù)的形狀和內(nèi)容。
清單 1 顯示了一個(gè)簡(jiǎn)單的 JavaCC .jj 腳本,它類似于您在第 1 部分中看到的腳本。為簡(jiǎn)便起見(jiàn),我只顯示了結(jié)果。
清單 1. simpleLang 的 JavaCC 語(yǔ)法
void simpleLang() : {} { addExpr() <EOF> }
void addExpr() : {} { integerLiteral() ( "+" integerLiteral() )? }
void integerLiteral() : {} { <INT> }
SKIP : { " " | "\t" | "\n" | "\r" }
TOKEN : { < INT : ( ["0" - "9"] )+ > }
|
該語(yǔ)法說(shuō)明了該語(yǔ)言中的合法表達(dá)式包含:
- 單個(gè)整數(shù)文字,或
- 一個(gè)整數(shù)文字,后面跟一個(gè)加號(hào),再跟另一個(gè)整數(shù)文字。
對(duì)應(yīng)的 JJTree .jjt 腳本(再次聲明,略有簡(jiǎn)化)看上去可能如下:
清單 2. 等價(jià)于清單 1 中的 JavaCC 語(yǔ)法的 JJTree
SimpleNode simpleLang() : #Root {} { addExpr() <EOF> { return jjtThis; }}
void addExpr() : {} { integerLiteral()
( "+" integerLiteral() #Add(2) )? }
void integerLiteral() : #IntLiteral {} { <INT> }
SKIP : { " " | "\t" | "\n" | "\r" }
TOKEN : { < INT : ( ["0" - "9"] )+ > }
|
該腳本對(duì)您已經(jīng)看到的腳本添加了一些新的語(yǔ)法特性。現(xiàn)在,我們只討論突出顯示的部分。以后,我會(huì)詳細(xì)說(shuō)明。
逐句說(shuō)明 JJTree 語(yǔ)法
首先請(qǐng)注意,最頂層的 simpleLang()
結(jié)果的 JavaCC 的過(guò)程性語(yǔ)法現(xiàn)在指定了一個(gè)返回類型: SimpleNode
。它與嵌入的 Java 操作 return jjtThis
(有一點(diǎn)為 JJTree 虛張聲勢(shì))一起指定了從應(yīng)用程序代碼調(diào)用解析器的 simpleLang()
方法將返回解析樹(shù)的根,然后這個(gè)根將用于樹(shù)遍歷。
在 JavaCC 中,解析器調(diào)用看上去如下:
SimpleParser parser = new SimpleParser(new StringReader( expression ));
parser.simpleLang();
|
而現(xiàn)在看上去象下面這樣:
SimpleParser parser = new SimpleParser(new StringReader( expression ));
SimpleNode rootNode = parser.simpleLang();
|
注:所抓取的根節(jié)點(diǎn)并不僅僅是 SimpleNode
類型。它其實(shí)是 Root
類型,正如 清單 2 中的 #Root
偽指令所指定的(雖然您不會(huì)在上述調(diào)用代碼中那樣使用)。 Root
是 SimpleNode
子代,就象 JJTree 生成的解析器構(gòu)造的每個(gè)節(jié)點(diǎn)一樣。我將在下面向您顯示 SimpleNode
的一些內(nèi)置方法。
addExpr()
結(jié)果中的 #Add(2)
構(gòu)造與上述的 #Root
偽指令不同,體現(xiàn)在以下幾方面;
- 它是參數(shù)化的。樹(shù)構(gòu)建器在構(gòu)造樹(shù)期間使用節(jié)點(diǎn)堆棧;沒(méi)有參數(shù)的節(jié)點(diǎn)構(gòu)建器的缺省行為是將自己放在正在構(gòu)造的解析樹(shù)的頂部,將所有節(jié)點(diǎn)彈出在同一個(gè) 節(jié)點(diǎn)作用域 中創(chuàng)建的節(jié)點(diǎn)堆棧,并把自己提升到那些節(jié)點(diǎn)父代的位置。參數(shù)
2
告訴新的父節(jié)點(diǎn)(在此示例中是一個(gè) Add
節(jié)點(diǎn))要恰好采用 兩個(gè)子節(jié)點(diǎn),它們是 下一段文字 中描述的兩個(gè) IntLiteral
子節(jié)點(diǎn)。JJTree 文檔更詳細(xì)地描述了這個(gè)過(guò)程。使用好的調(diào)試器在運(yùn)行時(shí)遍歷解析樹(shù)是另一個(gè)寶貴的輔助方法,它有助于理解樹(shù)構(gòu)建在 JJTree 中是如何工作的。
-
將
#Root
偽指令放在其結(jié)果的主體之外表示 每次 遍歷該結(jié)果時(shí)都會(huì)生成一個(gè) Root
節(jié)點(diǎn)(而在此特定示例中,只允許發(fā)生一次),這一點(diǎn)具有同等的重要性 。但是,將 #Add(2)
偽指令放在可選的“零或一”項(xiàng)中表示僅當(dāng)在解析期間遍歷包含它的選擇子句時(shí) ― 換句話說(shuō),當(dāng)該結(jié)果表示一個(gè)真的加法操作時(shí) ― 才 有條件地 生成一個(gè) Add
節(jié)點(diǎn)。當(dāng)發(fā)生這種情況時(shí),會(huì)遍歷 integerLiteral()
兩次,每次調(diào)用時(shí)都將一個(gè) IntLiteral
節(jié)點(diǎn)添加到樹(shù)上。這兩個(gè) IntLiteral
節(jié)點(diǎn)都成為調(diào)用它們的 Add
節(jié)點(diǎn)的子節(jié)點(diǎn)。但是,如果正在解析的表達(dá)式是單個(gè)整數(shù),那么作為結(jié)果的 IntLiteral
節(jié)點(diǎn)將直接成為 Root
的一個(gè)子節(jié)點(diǎn)。
一圖勝千言(引用一句古老的諺語(yǔ))。以下是由上述語(yǔ)法生成的兩種類型的解析樹(shù)的圖形表示:
圖 1:?jiǎn)蝹€(gè)整數(shù)表達(dá)式的解析樹(shù)
圖 2:加法操作的解析樹(shù)
讓我們更詳細(xì)地研究 SimpleNode
的類層次結(jié)構(gòu)。
使用解析樹(shù)
在 .jjt 腳本中聲明的每個(gè)節(jié)點(diǎn)都指示解析器生成 JJTree SimpleNode
的一個(gè)子類。接下來(lái), SimpleNode
又實(shí)現(xiàn)名為 Node
的 Java 接口。這兩個(gè)類的源文件都是由 JJTree 腳本和定制 .jj 文件一起自動(dòng)生成的。 清單 1 顯示了定制 .jj 文件的當(dāng)前示例。在當(dāng)前示例中,JJTree 還提供了您自己的 Root
、 Add
和 IntLiteral
類以及沒(méi)有在這里看到的一些附加的助手類的源文件。
所有 SimpleNode
子類都繼承了有用的行為。 SimpleNode
方法 dump()
就是這樣一個(gè)例子。它還充當(dāng)了我以前的論點(diǎn)(使用解析樹(shù)使調(diào)試更容易,從而縮短開(kāi)發(fā)時(shí)間)的示例。以下三行客戶機(jī)端代碼的代碼片段實(shí)例化了解析器、調(diào)用解析器、抓取所返回的解析樹(shù),并且將一個(gè)簡(jiǎn)單的解析樹(shù)的文本表示轉(zhuǎn)儲(chǔ)到控制臺(tái):
SimpleParser parser = new SimpleParser(new StringReader( expression ));
SimpleNode rootNode = parser.simpleLang();
rootNode.dump();
|
圖 2
中的樹(shù)的調(diào)試輸出是:
Root
Add
IntLiteral
IntLiteral
|
輔助導(dǎo)航
另一個(gè)有用的內(nèi)置 SimpleNode
方法是 jjtGetChild(int)
。當(dāng)您在客戶機(jī)端向下瀏覽解析樹(shù),并且遇到 Add
節(jié)點(diǎn)時(shí),您會(huì)要抓取它的 IntLiteral
子節(jié)點(diǎn)、抽取它們表示的整數(shù)值,并將這些數(shù)字加到一起 ― 畢竟,那是用來(lái)練習(xí)的。假設(shè)下一段代碼中顯示的 addNode
是表示我們感興趣的 Add
類型節(jié)點(diǎn)的變量,那我們就可以訪問(wèn) addNode
的兩個(gè)子節(jié)點(diǎn)。( lhs
和 rhs
分別是 左邊(left-hand side)和 右邊(right-hand side)的常用縮寫(xiě)。)
SimpleNode lhs = addNode.jjtGetChild( 0 );
SimpleNode rhs = addNode.jjtGetChild( 1 );
|
即使到目前為止您已經(jīng)執(zhí)行了所有操作,但您仍然沒(méi)有足夠的信息來(lái)計(jì)算該解析樹(shù)表示的算術(shù)運(yùn)算的結(jié)果。您的當(dāng)前腳本已經(jīng)省略了一個(gè)重要的細(xì)節(jié):樹(shù)中的兩個(gè) IntLiteral
節(jié)點(diǎn)實(shí)際上不包含它們聲稱要表示的整數(shù)。那是因?yàn)楫?dāng)記號(hào)賦予器(tokenizer)在輸入流中遇到它們時(shí),您沒(méi)有將它們的值保存到樹(shù)中;您需要修改 integerLiteral()
結(jié)果來(lái)執(zhí)行該操作。您還需要將一些簡(jiǎn)單的存取器方法添加到 SimpleNode
。
保存和恢復(fù)狀態(tài)
要將所掃描的記號(hào)的值存儲(chǔ)到適當(dāng)?shù)墓?jié)點(diǎn)中,將以下修改添加到 SimpleNode
:
public class SimpleNode extends Node
{
String m_text;
public void setText( String text ) { m_text = text; }
public String getText() { return m_text; }
...
}
|
將 JJTree 腳本中的以下結(jié)果:
void integerLiteral() : #IntLiteral {} <INT> }
|
更改成:
void integerLiteral() : #IntLiteral { Token t; }
{ t=<INT> { jjtThis.setText( t.image );} }
|
該結(jié)果抓取它剛在 t.image
中遇到的整數(shù)的原始文本值,并使用您的 setText()
setter 方法將該字符串存儲(chǔ)到當(dāng)前節(jié)點(diǎn)中。 清單 5 中的客戶機(jī)端 eval()
代碼顯示了如何使用相應(yīng)的 getText()
getter 方法。
可以很容易地修改 SimpleNode.dump()
,以提供任何節(jié)點(diǎn)的 m_text
值供其在解析期間存儲(chǔ) ― 我把這作為一個(gè)眾所周知的練習(xí)留給您來(lái)完成。這將讓您更形象地理解在進(jìn)行調(diào)試時(shí)解析樹(shù)看起來(lái)是什么樣子。例如,如果您解析了“42 + 1”,略經(jīng)修改的 dump()
例程可以生成以下有用的輸出:
Root
Add
IntLiteral[42]
IntLiteral[1]
|
組合:XQuery 的 BNF 代碼片段
讓我們通過(guò)研究實(shí)際語(yǔ)法的一個(gè)代碼片段來(lái)進(jìn)行組合和總結(jié)。我將向您演示一段非常小的 XQuery 的 BNF 子集,這是 XML 的查詢語(yǔ)言的 W3C 規(guī)范(請(qǐng)參閱 參考資料)。我在這里所說(shuō)的大多數(shù)內(nèi)容也適用于 XPath,因?yàn)檫@兩者共享了許多相同的語(yǔ)法。我還將簡(jiǎn)要地研究運(yùn)算符優(yōu)先級(jí)的問(wèn)題,并將樹(shù)遍歷代碼推廣到成熟的遞歸例程中,該例程可以處理任意復(fù)雜的解析樹(shù)。
清單 3
顯示了您要使用的 XQuery 語(yǔ)法片段。這段 BNF 摘自 2002 年 11 月 15 日的工作草案:
清單 3:一部分 XQuery 語(yǔ)法
[21] Query ::= QueryProlog QueryBody
...
[23] QueryBody ::= ExprSequence?
[24] ExprSequence ::= Expr ( "," Expr )*
[25] Expr ::= OrExpr
...
[35] RangeExpr ::= AdditiveExpr ( "to" AdditiveExpr )*
[36] AdditiveExpr ::= MultiplicativeExpr (("+" | "-") MultiplicativeExpr )*
[37] MultiplicativeExpr ::= UnionExpr (("*" | "div" | "idiv" | "mod") UnaryExpr )*
...
|
您將要構(gòu)建一個(gè)剛好適合的 JJTree 語(yǔ)法腳本來(lái)處理結(jié)果 [36] 和 [37] 中的 +
、 -
、 *
和 div
運(yùn)算符,而且簡(jiǎn)單地假設(shè)該語(yǔ)法所知道的唯一數(shù)據(jù)類型是整數(shù)。該樣本語(yǔ)法 非常小,并不能妥善處理 XQuery 支持的豐富的表達(dá)式和數(shù)據(jù)類型。但是,如果您要為更大、更復(fù)雜的語(yǔ)法構(gòu)建解析器,它應(yīng)該能給您使用 JavaCC 和 JJTree 的入門(mén)知識(shí)。
清單 4
顯示了 .jjt 腳本。請(qǐng)注意該文件頂部的 options{}
塊。這些選項(xiàng)(還有許多其它可用選項(xiàng)開(kāi)關(guān))指定了其中樹(shù)構(gòu)建在本例中是以 多重 方式運(yùn)行的,即節(jié)點(diǎn)構(gòu)造器用于顯式地命名所生成節(jié)點(diǎn)的類型。備用方法(不在這里研究)是結(jié)果只將 SimpleNode
節(jié)點(diǎn)提供給解析樹(shù),而不是它的子類。如果您想要避免擴(kuò)散節(jié)點(diǎn)類,那么該選項(xiàng)很有用。
還請(qǐng)注意原始的 XQuery BNF 經(jīng)常將多個(gè)運(yùn)算符組合到同一個(gè)結(jié)果中。在 清單 4中,我已經(jīng)將這些運(yùn)算符分離到 JJTree 腳本中的單獨(dú)結(jié)果中,因?yàn)檫@讓客戶機(jī)端的代碼更簡(jiǎn)單。要進(jìn)行組合,只需存儲(chǔ)已掃描的運(yùn)算符的值,就象對(duì)整數(shù)所進(jìn)行的操作一樣。
清單 4:清單 3 中的 XQuery 語(yǔ)法的 JJTree 腳本
options {
MULTI=true;
NODE_DEFAULT_VOID=true;
NODE_PREFIX="";
}
PARSER_BEGIN( XQueryParser )
package examples.example_2;
public class XQueryParser{}
PARSER_END( XQueryParser )
SimpleNode query() #Root : {} { additiveExpr() <EOF> { return jjtThis; }}
void additiveExpr() : {} { subtractiveExpr()
( "+" subtractiveExpr() #Add(2) )* }
void subtractiveExpr() : {} { multiplicativeExpr()
( "-" multiplicativeExpr() #Subtract(2) )* }
void multiplicativeExpr() : {} { divExpr() ( "*" divExpr() #Mult(2) )* }
void divExpr() : {} { integerLiteral()
( "div" integerLiteral() #Div(2) )* }
void integerLiteral() #IntLiteral : { Token t; }
{ t=<INT> { jjtThis.setText(t.image); }}
SKIP : { " " | "\t" | "\n" | "\r" }
TOKEN : { < INT : ( ["0" - "9"] )+ > }
|
該 .jjt 文件引入了幾個(gè)新的特性。例如,該語(yǔ)法中的運(yùn)算結(jié)果現(xiàn)在是 迭代的 :通過(guò)使用 *
(零次或多次)發(fā)生指示符來(lái)表示它們的可選的第二項(xiàng),這與 清單 2 中的 ?
(零次或一次)表示法相反。該腳本所提供的解析器可以解析任意長(zhǎng)的表達(dá)式,如“1 + 2 * 3 div 4 + 5”。
實(shí)現(xiàn)優(yōu)先級(jí)
該語(yǔ)法還知道 運(yùn)算符優(yōu)先級(jí)。例如,您期望乘法的優(yōu)先級(jí)比加法高。在實(shí)際例子中,這表示諸如“1 + 2 * 3”這樣的表達(dá)式將作為“1 + ( 2 * 3 )”進(jìn)行求值,而不是“( 1 + 2 ) * 3”。
優(yōu)先級(jí)是通過(guò)使用級(jí)聯(lián)樣式實(shí)現(xiàn)的,其中每個(gè)結(jié)果會(huì)調(diào)用緊隨其后的較高優(yōu)先級(jí)的結(jié)果。級(jí)聯(lián)樣式和節(jié)點(diǎn)構(gòu)造的位置和格式保證了以正確的結(jié)構(gòu)生成解析樹(shù),這樣樹(shù)遍歷可以正確執(zhí)行。用一些直觀圖形也許更易于理解這一點(diǎn)。
圖 3
顯示了由此語(yǔ)法生成的解析樹(shù),它可以讓您正確地將“1 + 2 * 3”當(dāng)作“1 + ( 2 * 3 )”進(jìn)行求值。請(qǐng)注意, Mult
運(yùn)算符與它的項(xiàng)之間的聯(lián)系比 Plus
更緊密,而這正是您希望的:
圖 3. 結(jié)構(gòu)正確的樹(shù)
而 圖 4顯示的樹(shù)(該語(yǔ)法 不會(huì)生成這樣的樹(shù))表示您(錯(cuò)誤地)想要將該表達(dá)式當(dāng)作“(1 + 2) * 3”求值。
圖 4. 結(jié)構(gòu)不正確的樹(shù)
遍歷解析樹(shù)客戶機(jī)端
就象我曾答應(yīng)的,我將用客戶機(jī)端代碼的清單作為總結(jié),該清單將調(diào)用該解析器并遍歷它生成的解析樹(shù),它使用簡(jiǎn)單而功能強(qiáng)大的遞歸 eval()
函數(shù)對(duì)樹(shù)遍歷時(shí)遇到的每個(gè)節(jié)點(diǎn)執(zhí)行正確操作。 清單 5中的注釋提供了關(guān)于內(nèi)部 JJTree 工作的附加詳細(xì)信息。
清單 5. 可容易泛化的 eval() 例程
// return the arithmetic result of evaluating 'query'
public int parse( String query )
//------------------------------
{
SimpleNode root = null;
// instantiate the parser
XQueryParser parser = new XQueryParser( new StringReader( query ));
try {
// invoke it via its topmost production
// and get a parse tree back
root = parser.query();
root.dump("");
}
catch( ParseException pe ) {
System.out.println( "parse(): an invalid expression!" );
}
catch( TokenMgrError e ) {
System.out.println( "a Token Manager error!" );
}
// the topmost root node is just a placeholder; ignore it.
return eval( (SimpleNode) root.jjtGetChild(0) );
}
int eval( SimpleNode node )
//-------------------------
{
// each node contains an id field identifying its type.
// we switch on these. we could use instanceof, but that's less efficient
// enum values such as JJTINTLITERAL come from the interface file
// SimpleParserTreeConstants, which SimpleParser implements.
// This interface file is one of several auxilliary Java sources
// generated by JJTree. JavaCC contributes several others.
int id = node.id;
// eventually the buck stops here and we unwind the stack
if ( node.id == JJTINTLITERAL )
return Integer.parseInt( node.getText() );
SimpleNode lhs = (SimpleNode) node.jjtGetChild(0);
SimpleNode rhs = (SimpleNode) node.jjtGetChild(1);
switch( id )
{
case JJTADD : return eval( lhs ) + eval( rhs );
case JJTSUBTRACT : return eval( lhs ) - eval( rhs );
case JJTMULT : return eval( lhs ) * eval( rhs );
case JJTDIV : return eval( lhs ) / eval( rhs );
default :
throw new java.lang.IllegalArgumentException(
"eval(): invalid operator!" );
}
}
|
|
結(jié)束語(yǔ)
如果您想要查看可以處理許多實(shí)際 XQuery 語(yǔ)法的功能更豐富的 eval()
函數(shù)版本,歡迎下載我的開(kāi)放源碼 XQuery 實(shí)現(xiàn)(XQEngine)的副本(請(qǐng)參閱 參考資料 )。它的 TreeWalker.eval()
例程 例舉了 30 多種 XQuery 節(jié)點(diǎn)類型。還提供了一個(gè) .jjt 腳本。
參考資料
關(guān)于作者
|
|
Howard Katz 居住在加拿大溫哥華,他是 Fatdog Software 的唯一業(yè)主,該公司專門(mén)致力于開(kāi)發(fā)搜索 XML 文檔的軟件。在過(guò)去的大約 35 年里,他一直是活躍的程序員(一直業(yè)績(jī)良好),并且長(zhǎng)期為計(jì)算機(jī)貿(mào)易出版機(jī)構(gòu)撰寫(xiě)技術(shù)文章。Howard 是 Vancouver XML Developer's Association 的共同主持人,還是 Addison Wesley 即將出版的書(shū)籍 The Experts on XQuery的編輯,該書(shū)由 W3C 的 Query 工作組成員合著,概述了有關(guān) XQuery 的技術(shù)前景。他和他的妻子夏天去劃船,冬天去邊遠(yuǎn)地區(qū)滑雪。可以通過(guò) howardk@fatdog.com與 Howard 聯(lián)系。
|