級別: 初級
JoAnn P. Brereton
, 高級軟件工程師,IBM
2004 年 5 月 01 日
JavaCC 是一個功能極其強大的‘編譯器的編譯器’工具,可用于編制上下文無關的語法。本文演示了如何將 JavaCC 用于支持終端用戶對 DB2 UDB 數據庫編制簡單的布爾查詢。
JavaCC 簡介
許多基于 Web 的項目都包含即席(ad-hoc)查詢系統以允許終端用戶搜索信息。因此,終端用戶會需要某種語言來表達他們所希望搜索的內容。以前,用戶查詢語言的定義極其簡單。如果終端用戶滿足于使用與最典型的 Google 搜索一般簡單的語言,那么 Java 的 StringTokenizer 對于解析任務就綽綽有余了。然而,如果用戶希望有一種更健壯的語言,比如要添加括號和“AND”/“OR”邏輯,那么我們很快就會發現我們需要更強大的工具。我們需要一種方法,用以首先定義用戶將要使用的語言,然后用該定義解析相應的條目并且對各種后端數據庫制定正確的查詢。
這就是工具 JavaCC 出現的原因。JavaCC 代表“Java? Compiler Compiler”,是對 YACC(“Yet Another Compiler Compiler”)的繼承(YACC 是 AT&T 為了構建 C 和其他高級語言解析器而開發的一個基于 C 的工具)。YACC 和其伙伴詞法記號賦予器(tokenizer)——“Lex”——接收由常用的巴科斯-諾爾范式(Backus-Nauer form,又稱 Bacchus Normal Form,BNF)形式的語言定義的輸入,并生成一個“C”程序,用以解析該語言的輸入以及執行其中的功能。JavaCC 與 YACC 一樣,是為加快語言解析器邏輯的開發過程而設計的。但是,YACC 生成 C 代碼,而 JavaCC 呢,正如您想像的那樣,JavaCC 生成的是 Java 代碼。
JavaCC 的歷史極具傳奇色彩。它起源于 Sun 公司的“Jack”。Jack 后來輾轉了幾家擁有者,比如著名的 Metamata 和 WebGain,最后變成了 JavaCC,然后又回到了 Sun。Sun 公司最后在 BSD 的許可下將它作為開放源代碼的代碼發布。JavaCC 目前的 Web 主頁是 http://javacc.net.java.net。
JavaCC 的長處在于它的簡單性和可擴展性。要編譯由 JavaCC 生成的 Java 代碼,無需任何外部 JAR 文件或目錄。僅僅用基本的 Java 1.2 版編譯器就可以進行編譯。而該語言的布局也使得它易于添加產生式規則和行為。該 Web 站點甚至描述了如何編制異常以便給出用戶合適的語法提示。
問題定義
讓我們假設您有一位客戶在一個出租視頻節目的商店里,該商店擁有一個簡單的電影數據庫。該數據庫包含表 MOVIES、ACTORS 和 KEYWORDS。MOVIES 表列舉他商店中每部電影的相關數據,即如每部電影的名稱和導演等內容。ACTORS 表列舉每部電影中的演員姓名。而 KEYWORDS 表則列舉描述電影的詞語,例如“action”、“drama”、“adventure”等等。
客戶希望能夠對該數據庫發出稍微復雜的查詢。例如,他想輸入以下形式的查詢
actor = "Christopher Reeve" and keyword=action and keyword=adventure
|
并且希望返回由 Christopher Reeve 主演的 Superman 系列電影。他還希望像下面這樣用括號來說明求值次序以區分查詢
(actor = "Christopher Reeve" and keyword=action) or keyword=romance
|
這樣可能返回不是由 Christopher Reeve 主演的電影
actor = "Christopher Reeve" and (keyword=action or keyword=romance)
|
這樣則總會返回 Christopher Reeve 主演的電影。
解決方案
對于該任務,您將分兩個階段來定義解決方案。在第 1 階段中,您將用 JavaCC 定義語言,要確保能夠正確解析終端用戶的查詢。在第 2 階段中,您將向 JavaCC 代碼添加行為以產生 DB2? SQL 代碼,從而確保返回正確的電影來響應終端用戶的查詢。
階段 1 - 定義用戶的查詢語言
將在名為 UQLParser.jj 的文件里定義該語言。該文件將被 JavaCC 工具編譯成為一組 .java 類型的 Java 類文件。要在 JJ 文件中定義語言,您需要做以下 5 件事:
- 定義解析環境
- 定義“空白”
- 定義“標記(token)”
- 按照標記定義語言本身的語法
- 定義每個解析階段中將發生的行為
您可以通過所展示的代碼段定義自己的 UQLParser.jj 文件,也可以通過本文的相關代碼進行效仿。對于步驟 1 到 4,在 JavaCCPaper/stage1/src 中使用 UQLParser.jj 的副本。而步驟 5 則在 JavaCCPaper/stage2/src 中進行。樣本數據庫的 DDL 可以在 JavaCCPaper/moviedb.sql 中找到。如果使用相同的用戶標識創建數據庫和運行解析器,該實例將運行得最好。Ant 文件(build.xml)可用于加快編譯過程。
步驟 1. 定義解析環境
JavaCC .jj 文件通過執行 JavaCC 將被轉換為 .java 文件。JavaCC 將獲取 .jj 文件里 PARSER_BEGIN 與 PARSER_END 的中間部分并將之復制到 Java 結果文件中。作為解析器設計者,您可以將解析前后所有與解析器有關的動作置于該文件中。您還可以在其中將 Java 代碼鏈接到步驟 4 和 5 中將會定義的解析器動作上。
在以下所示的實例中,解析器相對比較簡單。構造函數 UQLParser 接收一個字符串輸入,通過 Java 的 java.io.StringReader 類將其讀入,然后調用另一個不可見的構造函數將 StringReader 強制轉換為 Reader。這里定義的惟一其他方法就是 static main 方法,該方法將在調用構造函數之后再調用迄今還未定義的名為 parse() 的方法。
正如您可能已猜到的,JavaCC 已經提供了一個 Java Reader 類的構造函數。而我們添加了基于字符串的構造函數,以便易于使用和測試。
清單 1. 解析器的 Java 環境
PARSER_BEGIN(UQLParser)
package com.demo.stage1;
import java.io.StringReader;
import java.io.Reader;
public class UQLParser {
/**
A String based constructor for ease of use.
**/
public UQLParser(String s)
{
this((Reader)(new StringReader(s)));
}
public static void main(String args[])
{
try
{
String query = args[0];
UQLParser parser = new UQLParser(query);
parser.parse();
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
PARSER_END(UQLParser)
|
步驟 2. 定義空白
在該語言中,您希望將空格、跳格、回車和換行作為分隔符處理,而不是將其忽略。這些字符都被稱為 空白。在 JavaCC 中,我們在 SKIP 區域中定義這些字符,如清單 2 中所示。
清單 2. 在 SKIP 區域中定義空白
/** Skip these characters, they are considered "white space" **/
SKIP :
{
" "
| "\\t"
| "\\r"
| "\\n"
}
|
步驟 3. 定義標記
接下來,您將定義該語言所識別的標記。 標記是將對解析程序有意義的解析字符串的最小單位。掃描輸入字符串以及判斷是何標記的過程稱作 記號賦予器(tokenizer)。在以下查詢中,
actor = "Christopher Reeve"
其標記為
- actor
- =
- "Christopher Reeve"
在您的語言中,您要將 actor 和等號(=)作為該語言中的保留標記,盡管字 if和 instanceof在 Java 語言中都是帶有特殊意義的保留標記。通過保留字和其他特殊標記,程序員希望解析器會逐字地識別這些字并為其指派特定的意義。如果您正在保留這些字,請繼續進行下去并且保留不等號(<>)和左右括號。還要保留名稱、導演和關鍵字以表示用于用戶搜索的特定字段。
要定義所有這些內容,請使用 JavaCC 的 TOKEN 指令。每個標記的定義都用尖括號(< 和 >)括起來。在冒號(:)的左邊給出標記的名稱,并在右邊給出正則表達式。正則表達式是定義將要匹配的文本部分的方式。在其最簡單的形式中,正則表達式可以匹配精確的字符序列。使用下列代碼來定義六個匹配精確字的標記和四個匹配符號的標記。當分析器看到任何一個字時,將會用符號 AND、OR、TITLE、ACTOR、DIRECTOR 或 KEYWORD 來加以匹配。在匹配符號之后,解析器將相應地返回 LPAREN、RPAREN、EQUALS 或 NOTEQUAL。清單 3 展示了 JavaCC 保留標記的定義。
清單 3. 定義保留標記
TOKEN: /*RESERVED TOKENS FOR UQL */
{
<AND: "and">
| <OR: "or">
| <TITLE: "title">
| <ACTOR: "actor">
| <DIRECTOR: "director">
| <KEYWORD: "keyword">
| <LPAREN: "(">
| <RPAREN: ")">
| <EQUALS: "=">
| <NOTEQUAL: "<>">
}
|
對于像“Christopher Reeve”一樣的字符串,您或許無法在我們的語言中將所有的演員姓名存儲為保留字。但是,您可以通過使用正則表達式定義的字符模式識別 STRING 或 QUOTED_STRING 類型的標記。 正則表達式是定義匹配模式的字符串。定義匹配所有字符串或引用字符串的正則表達式要比定義精確的字匹配更具技巧性。
您將定義一個由一個或更多字符系列構成的字符串(STRING),其中的有效字符為大小寫的 A 到 Z 以及數字 0 到 9。為了簡單起見,不考慮定影明星或電影名稱的重音字符或其他不規則體。您可以按下列方式將該模式寫為一個正則表達式。
<STRING : (["A"-"Z", "a"-"z", "0"-"9"])+ >
|
加號表示圍在括號中的模式(從 A 到 Z、a 到 z 或 0 到 9 中的任何字符)可依次出現一次或多次。在 JavaCC 中,您還可以用星號(*)來表示模式的零次或多次出現以及用問號(?)來表示 0 或 1 此重復。
QUOTED_STRING 就更具技巧性了。如果您定義一個以引號開頭,以引號結尾并在其中包含任何其他字符的字符串,那么該字符串就是一個 QUOTED_STRING。其正則表達式為 "\\"" (~["\\""])+ "\\""
,這肯定有些眼花繚亂的。簡單一點理解就是,由于引用字符本身對于 JavaCC 是有意義的,因此我們需要將對它的引用轉換為對我們的語言而非 JavaCC 是有意義的。為了轉換該引用,我們在它之前使用了一個反斜杠。字符顎化符號(~)意味著并非是針對 JavaCC 記號賦予器的。 (~["\\""])+
是對于一個或更多非引用字符的速寫。合在一起, "\\"" (~["\\""])+ "\\""
就意味著“一個引用加上一個或更多非引用再加上一個引用”。
您必須在保留字規則之后添加 STRING 和 QUOTED_STRING 的記號賦予器規則。保持該次序是極其重要的,因為記號賦予器規則在文件中出現的次序就是應用標記規則的次序。您需要確定“title”是被視作保留字而非字符串的。清單 4 中顯示了 STRING 和 QUOTED_STRING 標記的完整定義。
清單 4. 定義 STRING 和 QUOTED_STRING
TOKEN :
{
<STRING : (["A"-"Z", "0"-"9"])+ >
<QUOTED_STRING: "\\"" (~["\\""])+ "\\"" >
}
|
步驟 4. 按照標記定義語言
既然已經定義了標記,那么現在是時候按照標記來定義解析規則了。用戶輸入表達式形式的查詢。一個 表達式就是一系列由 布爾運算符and或 or連接的一個或更多查詢項。
為了表達這一點,我們需要編寫一個解析規則,也稱作 產生式。將清單 5 中的產生式寫入 JavaCC UQLParser.JJ 文件。
清單 5. expression() 產生式
void expression() :
{
}
{ queryTerm()
(
( <AND> | <OR> )
queryTerm() )*
}
|
當對 .jj 文件運行 Javacc 時,產生式將被轉換為方法。所有的 JavaCC 產生式方法的返回都必須為空。第一組花括號包含產生式方法所需的所有聲明。這里暫時為空。第二組花括號包含以 JavaCC 理解的方式所寫的產生式規則。請注意先前所定義的 AND 和 OR 標記的用法。還請注意,queryTerm() 是作為方法調用而寫的。實際上,queryTerm() 是另一個產生式方法。
現在,就讓我們定義 queryTerm() 產生式。queryTerm() 要么是一個單獨的判別式(例如 title="The Matrix"),要么是一個由括號括起來的表達式。JavaCC 中通過 expression() 遞歸地定義了 queryTerm(),這使得您可以通過清單 6 中所示的代碼簡明地總結該語言。
清單 6. JavaCC 中的 queryTerm() 產生式方法(UQLParser.jj)
void queryTerm() :
{
}
{
(<TITLE> | <ACTOR> |
<DIRECTOR> | <KEYWORD>)
( <EQUALS> | <NOTEQUAL>)
( <STRING> | <QUOTED_STRING> )
|
<LPAREN> expression() <RPAREN>
}
|
這就是我們所需的所有規則。兩個產生式中總結了全部的語言解析器。
將 JavaCC 當作測試驅動器
在這個時候,您應該已經有了一個有效的 JavaCC 文件。在進行到步驟 5 之前,您可以編譯并“運行”該程序以查看您的解析器運作是否正確。
隨本文一起提供的 ZIP 文件應包含了階段 1 的 JavaCC 示例文件 UQLParser.jj。將整個 ZIP 文件解壓到一個空目錄下。要編譯 stage1/UQLParser.jj,您首先需要下載 JavaCC 并根據 JavaCC Web 頁 上的指導進行安裝。為了簡單起見,請務必將 Javacc.bat 的執行路徑填入 PATH 環境變量中。編譯十分容易,將目錄更改為卸載 UQLParser.jj 的位置并輸入下列命令。
javacc "debug_parser " output_directory=.\\com\\demo\\stage1 UQLParser.jj
如果您愿意,也可以使用附帶的 Ant 文件 build.xml。您必須將上方的屬性文件調整為指向 JavaCC 安裝。在您第一次運行它時,JavaCC 將生成如清單 7 中所示的消息。
清單 7. UQLParser.jj 的編譯輸出
Java Compiler Compiler Version 3.2 (Parser Generator)
(type "javacc" with no arguments for help)
Reading from file UQLParser.jj . . .
File "TokenMgrError.java" does not exist. Will create one.
File "ParseException.java" does not exist. Will create one.
File "Token.java" does not exist. Will create one.
File "SimpleCharStream.java" does not exist. Will create one.
Parser generated successfully.
|
除了已提到的四個文件,JavaCC 還將產生 UQLParser.java、UQLParserConstants.java 和 UQLParserTokenManager.java。所有這些文件都被寫入了 com\\demo\\stage1 目錄。此時起,您就能夠編譯這些文件且無需向默認的運行時類路徑做任何添加了。如果 JavaCC 步驟運行成功,Ant 文件的默認目標將自動執行 Java 編譯。如果沒有成功,您可以用以下命令編譯頂層目錄(JavaCCPaper/stage1)的文件:
javac "d bin src\\com\\demo\\stage1\\*.java
Java 類文件一旦就位,您就可以通過向您所定義的 "main" java 方法輸入下列用戶示例查詢來測試新的解析器了。如果您正使用同一代碼,請從 JavaCCPaper/stage1 目錄開始并在命令行中輸入下列命令。
java "cp bin com.demo.stage1.UQLParser "actor = \\"Tom Cruise\\""
我們在 JavaCC 步驟中所使用的 -debug_parser
選項確保將輸出下列有用的跟蹤消息,以顯示用戶查詢是如何被解析的。其輸出應該如清單 8 中所示。
清單 8. 查詢 actor="Tom Cruise" 的 UQLParser 輸出
Call: parse
Call: expression
Call: queryTerm
Consumed token: <"actor">
Consumed token: <"=">
Consumed token: <<QUOTED_STRING>:
""Tom Cruise"">
Return: queryTerm
Return: expression
Consumed token: <<EOF>>
Return: parse
|
要測試帶括號表達式的遞歸路徑,請嘗試以下測試。
java "cp bin com.demo.stage1.UQLParser "(actor=\\"Tom Cruise\\" or actor=\\"Kelly McGillis\\") and keyword=drama"
這將產生清單 9 中的輸出。
清單 9. 查詢 (actor="Tom Cruise" or actor="Kelly McGillis") and keyword=drama 的 UQL1Parser 輸出
Call: parse
Call: expression
Call: queryTerm
Consumed token: <"(">
Call: expression
Call: queryTerm
Consumed token: <"actor">
Consumed token: <"=">
Consumed token: <<QUOTED_STRING>:
""Tom Cruise"">
Return: queryTerm
Consumed token: <"OR">
Call: queryTerm
Consumed token: <"actor">
Consumed token: <"=">
Consumed token: <<QUOTED_STRING>:
""Kelly McGillis"">
Return: queryTerm
Return: expression
Consumed token: <")">
Return: queryTerm
Consumed token: <"AND">
Call: queryTerm
Consumed token: <"keyword">
Consumed token: <"=">
Consumed token: <<STRING>: "drama">
Return: queryTerm
Return: expression
Consumed token: <<EOF>>
Return: parse
|
該輸出十分有用,因為它演示了通過 queryTerm 和 expression 的遞歸。queryTerm 的第一個實例實際上就是一個由兩個 queryTerm 組成的表達式。 圖 1展示了該解析路徑的圖形視圖。
圖 1. 解析用戶查詢的圖形表示
如果您對于 JavaCC 生成怎樣的 Java 代碼感到好奇,就想盡方法看一看(但不要試圖進行任何更改!)。您將找到以下內容。
UQLParser.java —— 在這一文件中,您將找到您在 UQLParser.jj 文件里的 PARSER_BEGIN 和 PARSER_END 之間所放置的代碼。您還會發現 JJ 產生式方法已經被改變為 Java 方法了。
例如,expression() 規則已將被擴展為清單 10 中的代碼了。
清單 10. UQLParser.javastatic final public void expression() throws ParseException {
trace_call("expression");
try {
queryTerm();
label_1:
while (true) {
switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
case AND:
case OR:
;
break;
default:
jj_la1[0] = jj_gen;
break label_1;
}
switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
case AND:
jj_consume_token(AND);
break;
case OR:
jj_consume_token(OR);
break;
default:
jj_la1[1] = jj_gen;
jj_consume_token(-1);
throw new ParseException();
}
queryTerm();
}
} finally {
trace_return("expression");
}
}
|
它與您最初在其中寫入 queryTerm()、AND 和 OR 所呈現的樣子有些相像,但其余的就是 JavaCC 所添加的解析細節。
UQLParserConstants.java —— 該文件易于得到。您所定義的所有標記都在這里。JavaCC 只不過將它們記錄在數組中并提供整型常數來引用該數組。清單 11 展示了 UQLParserConstants.java 的內容。
清單 11. UQLParserConstants.java/* Generated By:JavaCC: Do not edit this line.
UQLParserConstants.java */
package com.demo.stage1;
public interface UQLParserConstants {
int EOF = 0;
int AND = 5;
int OR = 6;
int TITLE = 7;
int ACTOR = 8;
int DIRECTOR = 9;
int KEYWORD = 10;
int LPAREN = 11;
int RPAREN = 12;
int EQUALS = 13;
int NOTEQUAL = 14;
int STRING = 15;
int QUOTED_STRING = 16;
int DEFAULT = 0;
String[] tokenImage = {
"<EOF>",
"\\" \\"",
"\\"\\\\t\\"",
"\\"\\\\r\\"",
"\\"\\\\n\\"",
"\\"and\\"",
"\\"or\\"",
"\\"title\\"",
"\\"actor\\"",
"\\"director\\"",
"\\"keyword\\"",
"\\"(\\"",
"\\")\\"",
"\\"=\\"",
"\\"<>\\"",
"<STRING>",
"<QUOTED_STRING>",
};
}
|
UQLParserTokenManager.java —— 這是一個嵌接文件。JavaCC 將該類用作記號賦予器。這是一段確定標記為什么的代碼。這里讓人感興趣的首要例程是 GetNextToken。解析器產生式方法將用該例程來判斷采用哪條路經。
SimpleCharStream.java —— UQLParserTokenManager 用該文件來表示將被解析的字符的 ASCII 流。
Token.java —— 其中提供了 Token 類來表示標記本身。本文的下一部分將演示 Token 類的用途。
TokenMgrError.java and ParseException—— 這些類分別表示記號賦予器和分析器中的異常狀況。
階段 2 - 給 JavaCC 代碼添加行為
注意:關于教程的這一部分,請查閱代碼的 stage2 子目錄。從這里開始所展示的 JJ 文件就是 JavaCCPaper/stage2/UQLParser.jj。為了運行示例 SQL 查詢,您還應該通過附帶的 moviedb.sql 文件創建 MOVIEDB 數據庫。請通過 db2 -tf moviedb.sql
執行 DDL。
既然已經進行了解析,我們就需要對單個表達式采取行動了。這一階段的目標是生成可運行的 DB2 SQL 查詢并將返回用戶期望的結果。
該過程應該首先從一個包含空白處的模板 SELECT,解析器將填寫此空白處。 清單 12中顯示了 SELECT 模板。解析器所生成的查詢或許不像人類 DBA 所寫的那樣為最佳的,但是它將返回終端用戶所期望的正確結果。
清單 12. SELECT 語句SELECT TITLE, DIRECTOR
FROM MOVIE
WHERE MOVIE_ID IN
(
-- parser will fill in here--
);
|
解析器填入的內容取決于它通過記號賦予器所采用的路徑。例如,如果用戶從上面輸入查詢:
(actor="Tom Cruise" or actor="Kelly McGillis") and keyword=drama"
那么解析器將根據 圖 2 在 SQL 查詢的遺漏部分中發出文本。它將回送括號,輸入終端 queryTerm 的子查詢并且用 INTERSECT 代替 AND 以及 UNION 代替 OR。
圖 2. SQL 查詢的解析器輸出
這將確保 SQL 查詢發出
(actor = "Tom Cruise" or actor = "Kelly McGillis") and keyword=drama
將如清單 13 中所示。
清單 13. 完整的 SELECT 語句SELECT TITLE, DIRECTOR
FROM MOVIE
WHERE MOVIE_ID IN
(
(
SELECT MOVIE_ID
FROM ACTOR
WHERE NAME='Tom Cruise'
UNION
SELECT
MOVIE_ID
FROM ACTOR
WHERE NAME='kelly McGillis'
)
INTERSECT
SELECT MOVIE_ID
FROM KEYWORD
WHERE KEYWORD='drama'
);
|
正如前面提到的,很可能存在更快、更優的方法來編寫這個特定的查詢,但是此 SQL 將生成正確的結果。DB2 優化器通常可以解決性能方面的不足。
因此,需要向 JavaCC 源代碼添加什么來生成該查詢呢?您必須添加動作和其他支持所定義語法的代碼。 動作是指為響應特定產生式而執行的 Java 代碼。在添加動作之前,首先要添加將向調用程序返回完整 SQL 的方法。為此,要在 JavaCC 文件的最上部分添加一個名為 getSQL()
的方法。您還應該給解析器的內部成員添加 private StringBuffer sqlSB。該變量將表示任何解析階段的當前 SQL 字符串。 清單 14 展示了 UQLParser.jj 的 PARSER_BEGIN/PARSER_END 部分。最后,在 main() 測試方法中添加一些代碼,用以輸出和執行所生成的 SQL 查詢。
清單 14. PARSER_BEGIN/PARSER_END 部分PARSER_BEGIN(UQLParser)
package com.demo.stage2;
import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.Statement;
import java.lang.StringBuffer;
import java.io.StringReader;
import java.io.Reader;
public class UQLParser {
private static StringBuffer sqlSB;
// internal SQL representation.
public UQLParser(String s)
{
this((Reader)(new StringReader(s)));
sqlSB = new StringBuffer();
}
public String getSQL()
{
return sqlSB.toString();
}
public static void main(String args[])
{
try
{
String query = args[0];
UQLParser parser =
new UQLParser(query);
parser.parse();
System.out.println("\\nSQL Query: " +
parser.getSQL());
// Note: This code assumes a
// default connection
// (current userid and password).
System.out.println("\\nResults of Query");
Class.forName(
"COM.ibm.db2.jdbc.app.DB2Driver"
).newInstance();
Connection con =
DriverManager.getConnection(
"jdbc:db2:moviedb");
Statement stmt =
con.createStatement();
ResultSet rs =
stmt.executeQuery(parser.getSQL());
while(rs.next())
{
System.out.println("Movie Title = " +
rs.getString("title") +
" Director = " +
rs.getString("director"));
}
rs.close();
stmt.close();
con.close();
}
catch(Exception e)
{
e.printStackTrace();
}
}
}
PARSER_END(UQLParser)
|
現在,填入由解析器來完成的動作。我們將先從一個容易的開始。在解析一個表達式的時候,解析器每當解析“AND”時就發出字“INTERSECT”,而解析“OR”時就發出“UNION”。為此,要在 expression 產生式中的 <AND> 和 <OR> 標記之后插入自包含的 Java 代碼塊。該代碼應向 sqlSB StringBuffer 追加 INTERSECT 或 UNION。清單 15 中顯示了這些代碼。
清單 15. expression 所執行的動作void expression() :
{
}
{ queryTerm()
(
( <AND>
{ sqlSB.append("\\nINTERSECT\\n"); }
| <OR>
{ sqlSB.append("\\nUNION\\n"); }
)
queryTerm() )*
}
|
產生式內需要執行多個動作。這些任務如下:
- 將搜索名稱映射到它們各自的 DB2 表和列上
- 保存比較器(comparator)標記
- 將比較字(comparand)轉換為 DB2 可以理解的形式,例如,除去 QUOTED_STRING 標記的雙引號
- 向 sqlSB 發送合適的子查詢
- 對于遞歸表達式的情況,隨之發出括號。
對于所有這些任務,您將需要一些局部變量。如清單 16 中所示,這些變量是在產生式中第一對花括號之間定義的。
清單 16. queryTerm() 的局部變量void queryTerm() :
{
Token tSearchName, tComparator, tComparand;
String sComparand, table, columnName;
}
|
第一個任務可用清單 17 中的代碼來完成。設置與所遇標記關聯的合適的 DB2 表和列。
清單 17. 將搜索名稱映射到 DB2 (
<TITLE> {table = "movie";
columnName = "title"; } |
<DIRECTOR> {table = "movie";
columnName = "director"; } |
<KEYWORD> {table = "keyword";
columnName = "keyword"; } |
<ACTOR> {table = "actor";
columnName = "name"; }
)
|
第二個任務可用清單 18 中的代碼來完成。保存標記以便可用于 SQL 緩沖區。
清單 18. 保存比較器 ( tComparator=<EQUALS> |
tComparator=<NOTEQUAL> )
|
第三個任務可用清單 19 中的代碼來完成。相應地設置比較字的值,如果有必要,就從 QUOTED_STRING 標記中除去雙引號。
清單 19. 準備比較字 tComparand=<STRING> {
sComparand = tComparand.image; }
|
tComparand=<QUOTED_STRING>
{ // need to get rid of quotes.
sComparand =
tComparand.image.substring(1,
tComparand.image.length() - 1);
}
|
第四個任務可用清單 20 中的代碼來完成。完整的查詢項被追加到了 sql 緩沖區。
清單 20. 編寫 SQL 表達式 {
sqlSB.append("SELECT MOVIE_ID FROM ").append(table);
sqlSB.append("\\nWHERE ").append(columnName);
sqlSB.append(" ").append(tComparator.image);
sqlSB.append(" '").append(sComparand).append("'");
}
|
最后對于遞歸表達式的情況,當解析器在表達式遞歸中看到括號時,就應該簡單地進行回送,如清單 21 中所示。
清單 21. 回送括號 <LPAREN>
{ sqlSB.append("("); }
expression()
<RPAREN>
{ sqlSB.append(")"); }
|
清單 22 展示了完整的 queryTerm() 產生式。
清單 22. 完整的 queryTerms() 產生式/**
* Query terms may consist of a parenthetically
* separated expression or may be a query criteria
* of the form queryName = something or
* queryName <> something.
*
*/
void queryTerm() :
{
Token tSearchName, tComparator, tComparand;
String sComparand, table, columnName;
}
{
(
<TITLE> {table = "movie";
columnName = "title"; } |
<DIRECTOR> {table = "movie";
columnName = "director"; } |
<KEYWORD> {table = "keyword";
columnName = "keyword"; } |
<ACTOR> {table = "actor";
columnName = "name"; }
)
( tComparator=<EQUALS> |
tComparator=<NOTEQUAL> )
(
tComparand=<STRING>
{ sComparand = tComparand.image; } |
tComparand=<QUOTED_STRING>
{ // need to get rid of quotes.
sComparand = tComparand.image.substring(1,
tComparand.image.length() - 1);
}
)
{
sqlSB.append("SELECT MOVIE_ID FROM ").append(table);
sqlSB.append("\\nWHERE ").append(columnName);
sqlSB.append(" ").append(tComparator.image);
sqlSB.append(" '").append(sComparand).append("'");
}
|
<LPAREN>
{ sqlSB.append("("); }
expression()
<RPAREN>
{ sqlSB.append(")"); }
}
|
像前面一樣編譯并運行 UQLParser.jj。訪問 UQLParser.java 并注意產生式規則是如何被整齊地插入生成代碼中的。清單 23 中展示了一個 expression() 方法的擴展實例。請注意 jj_consume_token 調用之后的代碼。
清單 23. UQLParser.java 中的 expression() 方法/**
* An expression is defined to be a queryTerm followed by zero
* or more query terms joined by either an AND or an OR. If two
* query terms are joined with * AND then both conditions must
* be met. If two query terms are joined with an OR, then
* one of the two conditions must be met.
*/
static final public void expression() throws ParseException {
trace_call("expression");
try {
queryTerm();
label_1:
while (true) {
switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
case AND:
case OR:
;
break;
default:
jj_la1[0] = jj_gen;
break label_1;
}
switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
case AND:
jj_consume_token(AND);
sqlSB.append("\\nINTERSECT\\n");
break;
case OR:
jj_consume_token(OR);
sqlSB.append("\\nUNION\\n");
break;
default:
jj_la1[1] = jj_gen;
jj_consume_token(-1);
throw new ParseException();
}
queryTerm();
}
} finally {
trace_return("expression");
}
}
|
按前面一樣運行該代碼。您必須在 CLASSPATH 中包含 db2java.zip。這次,當您運行
java 'cp bin;c:/sqllib/db2java.zip com.demo.stage2.UQLParser "(actor=\\"Tom Cruise\\" or actor=\\"Kelly McGillis\\") and keyword=drama"
時,它將生成清單 24 中的輸出。
清單 24. 查詢 (actor="Tom Cruise" or actor="Kelly McGillis") and keyword=drama 的 UQL2Parser 輸出Call: parse
Call: expression
Call: queryTerm
Consumed token: <"(">
Call: expression
Call: queryTerm
Consumed token: <"actor">
Consumed token: <"=">
Consumed token: <<QUOTED_STRING>:
""Tom Cruise"">
Return: queryTerm
Consumed token: <"or">
Call: queryTerm
Consumed token: <"actor">
Consumed token: <"=">
Consumed token: <<QUOTED_STRING>:
""Kelly McGillis"">
Return: queryTerm
Return: expression
Consumed token: <")">
Return: queryTerm
Consumed token: <"and">
Call: queryTerm
Consumed token: <"keyword">
Consumed token: <"=">
Consumed token: <<STRING>: "drama">
Return: queryTerm
Return: expression
Consumed token: <<EOF>>
Return: parse
SQL Query: SELECT TITLE,DIRECTOR
FROM MOVIE
WHERE MOVIE_ID IN (
(SELECT MOVIE_ID FROM actor
WHERE name = 'Tom Cruise'
UNION
SELECT MOVIE_ID FROM actor
WHERE name = 'Kelly McGillis')
INTERSECT
SELECT MOVIE_ID FROM keyword
WHERE keyword = 'drama')
Results of Query
Movie Title = Top Gun Director = Tony Scott
Movie Title = Witness Director = Peter Weir
|
嘗試更多使用您的解析器的查詢。試一試使用 NOTEQUAL 標記的查詢,比如 actor<>"Harrison Ford"。嘗試一些像“title=”一樣的非法查詢,看看將發生什么情況。通過非常少的幾行 JavaCC 代碼,您就生成了非常有效的終端用戶查詢語言。
最后要考慮的問題
JavaCC 除了提供解析器生成器之外,還提供 JJDOC 工具,用以編制巴科斯-諾爾范式(Bacchus-Nauer Form)表示的語法。JJDOC 可以使您易于向終端用戶提供他們所使用語言的描述。例如,在附帶代碼中提供的 ant 文件有一個“bnfdoc”目標。
JavaCC 還提供名為 JJTree 的工具。該工具提供樹和節點類,使您易于將代碼分成單離的解析和動作類。繼續該實例,您可以考慮為查詢編寫一個簡單優化器,以消除不必要的 INTERSECT 和 UNION。您可以通過訪問解析樹的節點以及合并相似的相鄰節點(例如,actor="Tom Cruise" 和 actor="Kelly McGillis")來完成該工作。
JavaCC 擁有一個豐富的語法庫。您在自己編寫解析器之前,一定要查看 JavaCC 的 examples 目錄,以便可能獲取已構建好的解決方案。
請務必閱讀 JavaCC Web 頁上的 Frequently Asked Questions 并訪問 comp.compilers.tools.javacc 上的 javacc 新聞組以便更好地理解 JavaCC 的所有功能和特性。
結束語
JavaCC 是一個健壯的工具,可用于定義語法并且易于在 Java 商業應用程序中包含該語法的解析和異常處理。通過本文,我們說明了 JavaCC 可用于為數據庫系統的終端用戶提供一種功能強大卻很簡單的查詢語言。
參考資料
關于作者
 |
| JoAnn Brereton 是 IBM 的 Software Group,Federal Software Services 的一位高級軟件工程師。她已為 IBM 編程近 20 年了。她最近的項目包括為 CBS、Warner Brothers 和 CNN 電視網構建視檔案搜索引擎。 |