由于项目原因又用到了以前没接触到的东东(JAVACC,ASTParser,Java Development Tooling (JDT) )

JavaParser 系列学习 - AST 简单介绍 通过深入学习 AST 的概念和使用 JavaParser 进行 AST 操作,您将能够更好地理解和应用代码分析和转换技术,提高代码质量和开发效率。通过遍历和操作 AST,我们可以获取代码的结构信息,进行静态分析、代码重构、自动化生成等一系列强大的编程工作。总结起来,本文简要介绍了 AST 的概念和作用,并示范了如何使用 JavaParser 解析和操作 AST。希望这篇文章能够为你提供有关 AST 和 JavaParser 的基础知识,并激发你进一步探索编程语言处理的世界的兴趣和热情。 阅读详情

由于项目原因又用到了以前没接触到的东东(JAVACC,ASTParser,Java Development Tooling (JDT) )

初步学习中.......加油啊!!!!!         JJTree, BNF ,EBNF(扩展的 BNF), XQuery

现在你可以定义自己的语言!

级别: 初级

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 件事:

  1. 定义解析环境
  2. 定义“空白”
  3. 定义“标记(token)”
  4. 按照标记定义语言本身的语法
  5. 定义每个解析阶段中将发生的行为

您可以通过所展示的代码段定义自己的 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.java


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);
        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 查询的分析器输出

这将确保 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() )*
}
			

queryTerm()

产生式内需要执行多个动作。这些任务如下:

  1. 将搜索名称映射到它们各自的 DB2 表和列上
  2. 保存比较器(comparator)标记
  3. 将比较字(comparand)转换为 DB2 可以理解的形式,例如,除去 QUOTED_STRING 标记的双引号
  4. 向 sqlSB 发送合适的子查询
  5. 对于递归表达式的情况,随之发出括号。

对于所有这些任务,您将需要一些局部变量。如清单 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 可用于为数据库系统的终端用户提供一种功能强大却很简单的查询语言。

ling jian:http://www-128.ibm.com/developerworks/cn/xml/x-javacc/part1/index.html

别自己写CDC了!聊聊Xilinx AXI4-Stream Clock Converter IP里的寄存器管道模式到底有多省资源 本文探讨了Xilinx AXI4-Stream Clock Converter IP核的寄存器管道模式在FPGA设计中的高效资源利用。通过对比手工CDC实现,详细解析了IP核的精简化同步架构和时序收敛优势,并提供了实战案例和优化策略,帮助工程师快速实现时钟域转换,显著提升设计效率和性能。 阅读详情

相关推荐

LLC谐振变换器设计实战:从工作原理到参数设计与调试

开关电源设计是电力电子领域的核心技术,其核心目标在于提升效率与功率密度。为实现这一目标,软开关技术应运而生,它通过精心控制开关器件的电压与电流波形,使其在开通或关断瞬间实现零电压或零电流切换,从而从根本上降低开关损耗。LLC谐振变换器正是这一技术的杰出代表,它通过引入谐振网络,巧妙地实现了主开关管的零电压开关和次级整流管的零电流开关。这种拓扑结构在服务器电源、通信设备及高端显示驱动等要求高效率、高功率密度的应用场景中价值显著。本文将以半桥LLC为例,深入剖析其工作模态与增益特性,并系统阐述其参数设计、仿真验

weixin_34129696的博客 557

javacc学习笔记

转自:http://blog.youkuaiyun.com/kmlzkma今天刚学,啥也不明白,网罗了一堆文档教程什么的,英文的中文的啥都有了,看了会最简单的,还凑合写出来,看看能说明白不. 看教程上的例子,学着写了一个,还真好使了,就是一个十进制的加法表达式,比如1+1呵呵,我知道等于2,可这个小程序不是算结果的,是检验表达式合法不,基本就这样了,得先写个什么.jj文件规范JavaCC的解析

石头记 2835

QGC 转发RTSP流 到RTMP服务器

需求: QGC安卓版在接收摄像机RTSP视频流的时候,需要将视频推送到指定的RTMP服务器。 分析: QGC在处理音视频的时候使用了GStreamer这套框架,本身这套框架是跨平台的,所以无需关系环境,只要想到相应位置,添加对应功能代码,然后配置好安卓端的依赖库即可。 添加位置: GstVideoReceiver.cpp -》start函数负责音视频的流的播放,只需在此处添加上封装好的推流函数即可。 同理在stop中添加停止推流功能。 方便起见,可在界面上配置rtmp的推流地.

qthello 2324

欢迎光临 Javen-Studio 新网址:http://javenstudio.org

 欢迎光临 Javen-Studio 新网址:http://javenstudio.org 

navychen的专栏 2万+

别再手动翻译PDF了!手把手教你用Zotero PDF Translate插件搞定外文文献(附最新GitHub下载链接)

本文详细介绍如何利用Zotero PDF Translate插件高效翻译外文文献,包括插件安装、配置及多引擎对比。通过划词翻译、段落翻译和全文翻译功能,显著提升科研工作效率,特别适合处理大量英文PDF文献的研究人员。

weixin_33712987的博客 771

FOC电机控制中电流采样延迟补偿的原理和方法

摘要: FOC电机控制中,电流采样、计算与PWM更新存在固有延迟(约1.5个PWM周期),导致相位滞后,限制电流环带宽。主流补偿方法包括: 一阶前向预测法:基于历史电流值预测未来电流,抵消延迟,工程应用广泛; 相位超前补偿:通过s/z域校正器设计实现等效预测; 状态观测器法(如龙伯格观测器):通过电机模型闭环估计状态,兼具噪声抑制和参数辨识能力,但复杂度高。实际需注意参数敏感性、离散化精度及延迟时间校准,避免过补偿风险。一阶预测法因平衡效果与复杂度成为工业首选。

jifengzhiling的博客 2231

javaCC---可读到可编程的桥梁

简单来说,javacc是一套词法、语法解析器。我们的java程序代码就是通过javacc进行前置的编译处理,对合法性进行校验,并生成对应的抽象语法树(AST),然后基于此做各种编译提示和编译优化的。 ...

cuglifangzheng的专栏 1856

innovus:Patch Wire(补丁线)

1)选中wire ,shift+A使用Edit Route功能,F3打开edit route对话框。线道Create Patch Wire,设置好层次和宽度。在修补一些min area的drc时,会有一些danling wire的drc冒出来,为了避免这些假错,可以使用工具patch wire功能。2)沿着选中的wire画线即可。如下图,patch wire和regular wire是能看出区别的。我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧?

m0_61544122的博客 1961

java 抽象语法树(AST)系列一:概述

java 抽象语法树解析

琼华 9538

SAP_通用模块-MASS批量操作技巧

SAP_MM模块-MASS/MM17批量更新主数据详细教程

风沙的博客 2164

《干货满满!AI 应用架构师打造 AI 驱动生产计划的实战技巧》

生产计划是制造企业的“大脑”——它决定了买什么物料、产多少产品、用什么设备、什么时候交货,直接影响企业的成本、效率和客户满意度。需求预测靠经验,误差常超20%;排产依赖Excel,调整一次要3天;供应链协同靠邮件,物料延迟导致停产屡见不鲜。作为AI应用架构师,我们的任务是用AI重新“激活”这个“大脑”——用数据驱动的预测替代经验判断,用智能优化替代人工排产,用实时协同替代被动响应。本文结合3个制造业实战案例5段可运行代码2个数字孪生模型,从架构设计→核心模块实现→落地踩坑。

移动开发前沿的博客 1081

datart本地开发环境搭建步骤及常见问题

1、概述 跑象的开源数据可视化框架,具体功能参考官方链接: datart: 新一代数据可视化开放平台,支持报表、仪表板、大屏、分析和可视化数据应用的敏捷构建。原davinci主创团队出品。 本篇文章主要讲解将datart下载到本地之后如何搭建开发环境,非部署手册(官方的部署手册很详细),这边主要针对datart进行二次开发学习,所以本篇主要内容为搭建前后端开发环境的步骤和一些问题,希望能提供给你一些参考。 2、前端搭建 将datart-master下载到本地解压之后,fro...

AVATAR 9482

知识图谱:综述

第一种理解:知识图谱本质上是语义网络。本体论是语义网络中最重要的一个要素。第二种理解:知识图谱也叫做多关系图,由多种类型的节点和多种类型的边组成。课程以第二种理解为主。

u013250861的博客 478

MAC下javacc系统变量设置(原因:javacc .jj文件,datart运行报错:datart.data.provider.calcite.parser.impl.SqlParserImpl)

cd 到目标文件下,使用 javacc ./*********.jj。这串代码在终端执行一次后可以生成暂时的系统变量,终端关闭后结束。你可以直接下载,但我没有尝试,我是使用git拉取下载。如果想要长期存在,你可以搜索mac系统环境变量添加。这时候你就可以使用javacc命令了。只要目录基本相同基本上没有问题。

qq_48959246的博客 857

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

简单学习Java中的抽象语法树(AST)

抽象语法树(AST)是一种数据结构,代表了一种编程语言的抽象语法。它通常在编译器中用来解析源代码和生成机器代码。在Java中,AST通常由一个解析器创建,该解析器读取源代码并创建一个树状结构,代表代码的语法和结构。树中的每个节点都代表一个不同的语法结构,如类、方法或语句。使用AST,编译器可以更容易地分析源代码并识别任何错误或问题。例如,编译器可以使用AST来检查类型错误、未声明的变量或不当的控制流。一般来说,AST是编译器和其他需要分析和处理编程语言语法的工具的重要工具。

BASK2311的博客 4674

pscad IEEE标准系统搭建说明

pscad IEEE标准系统搭建说明,包含典型的IEEE 9节点、14节点、30节点、39节点和118节点系统。内含标准测试系统的原始数据和潮流结果。

深入 Java 抽象语法树(AST)的奇妙世界

通过上述示例,我们可以看到如何利用抽象语法树(AST)来表示和匹配正则表达式。每个节点类型(如CharNodeConcatNodeUnionNode)都实现了特定的匹配逻辑。最终,通过组合这些节点,我们可以生成一个复杂的正则表达式,并对输入字符串进行匹配操作。这样的设计不仅清晰,而且便于扩展和维护。设置解析器:创建并配置ASTParser。解析源代码:将源代码解析成。访问节点:使用ASTVisitor遍历 AST。创建新节点:通过 AST 创建新的语法节点(如方法声明)。修改结构:使用。

2301_78976656的博客 3495

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

【datart】数据可视化工具datart在IDEA中进行代码调试

datart在idea中进行代码调试

masetLI的博客 2409

使用ASTParser(抽象语法树)获取类属性注释

   项目中有一个小任务,就是将项目中涵盖有哪些字段列举出来.思来想去,想到了AST可以读取到属性的注释,就免去了把字段获取后还需要填写中文名称的麻烦. 引入包 &lt;!-- https://mvnrepository.com/artifact/de.defmacro/eclipse-astparser --&gt;         &lt;dependency&gt;           ...

y_hai_yang的博客 5216

使用JDT ASTParser解析Java语句序列

让我们看一下如何使用Eclipse JDT中的ASTParser解析Java语句序列。 通过说一条语句序列,我的意思是来自如下方法的代码行: int i = 9; int j = i+1; 这是如何执行此操作的代码。 public static void main(String[] args) { ASTParser parser = ASTParser.newParser(AST.JLS3);...

阿志同学 1106

java AST 抽象语法树

AST学习

xinyuan的专栏 3476

Java抽象语法树AST浅析与使用

Java抽象语法树AST浅析与使用概述作用Java项目模型对象AST模型对象AST试图具体使用 概述 抽象语法树(Abstract Syntax Tree, AST)是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的结构,树的每个节点ASTNode都表示源码中的一个结构。Eclipse java的开发工具(JDT)提供了Java源代码的抽象语法树AST。抽象语法树就像是java文件的do...

peng425的博客 3万+

Eclipse JDT--ASTParser使用

创建ASTParser并将Java源文件解析成AST:[java] view plain copy// Initialize ASTParser          ASTParser parser = ASTParser.newParser(AST.JLS3); //initialize            parser.setKind(ASTParser.K_COMPILATION_UNIT)...

飞奔的菜猪的博客 5736

Datart使用说明

跑象团队新发布的开源平台 Datart安装问题记录

无语梦醒的博客 7260

补充:Java 中包含注解程序的 AST(抽象语法分析树)

在Java中,注解(Annotation)是一种特殊的注释,可以用于为程序元素(如类、方法、成员变量等)提供元数据信息。注解本身并不会改变程序的语义,而是提供了一种方式,使得程序员可以在代码中嵌入额外的信息,这些信息可以在编译时、运行时或代码生成时被处理。在Java的抽象语法树(AST)中,注解通常被表示为一种特殊的节点类型,即。节点可以用于表示源代码中的注解声明和注解实例。等也代表了Java源代码中的相应语法结构。这个AST中的其他节点类型如。它包含了注解的名称(

name_sakura的博客 1001

Eclipse JDT--ASTParser介绍

要解析Java源码,首先要建立ASTParser的实例。此过程最重要的一点是,你要告诉parser需要解析的内容类型。ASTParser支持对一下四种内容的解析:1. K_COMPILATION_UNIT: 一个编译单元,一般就是完整的Java文件2. K_STATEMENTS: Java statements,比如赋值语句,或是if语句块,while语句块等。此类型不需要文件是完整的编译单元,但需要是完整的statements。比如if语句块要作为一个完整的statement输入,否则会报错。3. K_E

Step Up 2万+

精准测试之ASTParser 解析含有emum 枚举方法的类文件的踩坑记

使用ASTParser 解析含有emum 枚举方法的类文件时,解析的结果时错误的。比如下面的文件解析后method 数据含有以下四个,其中FULL_AMOUNT不是一个method,对应的行范围也是不准的,结果导致我们在影响面评估中,评估该方法有变动但却找不到的问题,使得调用链路分析评估不准确。......

小生测试的博客 701
上一篇: 备忘录模式(Memento Pattern)
下一篇: JavaCC、解析树和 XQuery 语法
超级谷歌
博客等级 码龄20年 2271粉丝 286原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值