简介:在前面的文章中,ahuaxuan已经指出jackrabbit查询的主体流程,主体流程只能帮助大家稍微了解一下jackrabbit的 query module,要深入理解还得深入探索主体流程中的每一步。在这篇文章中ahuaxuan将着重描述AST的生成和QT(querynode tree)的生成。
回顾上一篇文章,我们可以知道jackrabbit中的查询手段之一:xpath。下面我们还拿上一篇文章的xpath示例:
Java代码
String queryPath2 =//hello/test3 [jcr:contains(@name, 'aa') and jcr:contains(@content, 'Maven') or @data1 = 'ahuaxuan'] order by jcr:score() descending, @name ascending
这句话的意思是:得到在world节点中包含Maven这个字符串的节点,并且按照jcr:score()降序,name这个property升序。和sql语句非常类似。
我们知道jackrabbit是通过lucene来执行查询任务,那么lucene是怎么知道这个语句中包含什么信息的呢。
要得到这个答案,我们不得不捡起我们的基础学科,编译原理。编译的目的是把一种语言翻译成另外一种语言,比如java的源代码需要编译成字节码,这个就是通过编译技术实现,很遗憾,字节码并不是机器语言,不能直接执行,所以字节码会被再次编译,生成机器语言。运行过程中,这些编译之后的代码也有可能再次被重新优化编译。所以天天使用java的同志们,还是要了解一下编译的作用。
有点偏题了,那么现在我们的目的是什么,就是把这个xpath语言翻译成另外一种语言。翻译成java中的数据结构,然后用户可以通过开发处理这些数据结构的代码来解析和使用这些数据结构,这里讲到的数据结构就是QT(其实QT也是一个语法树,只是这颗语法树能够让程序直接识别)。
在jackrabbit中,使用了javacc来生成AST,关于javacc的使用请google之,它的词法文件在源码中可以找到,XPath.jj。
AST在生成之后,是怎么转换成QT的呢?要回答这个问题,我们必须要理解一个设计模式,vistor模式
因为在AST转换成QT,以及从QT转成lucene的query对象时都使用了vistor,在asm中也是使用vistor,emma中也是使用vistor,看来它很火,口说无凭,有图为证:
从上图中可以看到在处理SimpleNode的时候使用了XPathVisitor,而在处理QueryNode的时候使用QueryNodeVistor.
一个简单的vistor模式如下:
Java代码
package org.apache.jackrabbit.core.query.xpath;
public interface XPathVisitor
{
public Object visit(SimpleNode node, Object data);
}
public class SimpleNode implements Node {
protected Node parent;
protected Node[] children;
protected int id;
protected XPath parser;
/**
* Accept the visitor. *
*/
public Object jjtAccept(XPathVisitor visitor, Object data) {
return visitor.visit(this, data);
}
/**
* Accept the visitor. *
*/
public Object childrenAccept(XPathVisitor visitor, Object data) {
if (children != null) {
for (int i = 0; i < children.length; ++i) {
data = children[i].jjtAccept(visitor, data);
}
}
return data;
}
}
从这两个简单的类的关系中,我们可以看到如下事实:SimpleNode类接受一个Vistor,然后在vistor的vist方法中,会处理该SimpleNode,换句话说,一个domain对象会拿另外一个process对象来处理自己。
另外一个值得注意的是SimpleNode其实就是AST上的节点。下面一个主要工作就是处理AST上的节点,并且生成QueryNode Tree。
在visitor模式中,处理数据结构的主要逻辑在是visit方法,那么下面我们就来看看XPathVisitor实现类XPathQueryBuilder的visit方法,该方法中集中了AST转成QT的关键逻辑。代码如下:
Java代码
public Object visit(SimpleNode node, Object data) {
QueryNode queryNode = (QueryNode) data;
switch (node.getId()) {
case JJTXPATH2:
queryNode = createPathQueryNode(node);
break;
case JJTROOT:
case JJTROOTDESCENDANTS:
if (queryNode instanceof PathQueryNode) {
((PathQueryNode) queryNode).setAbsolute(true);
} else {
exceptions.add(new InvalidQueryException(
"Unsupported root level query node: " + queryNode));
}
break;
case JJTSTEPEXPR:
………………………..
return queryNode;
}
一段很长的代码,这段代码的主要作用是将AST,也就是SimpleNode组成的tree转换成QueryNodeTree,原来节点的类型在simplenode中是以id来表示,而在转换之后的QueryNodeTree中,则是以QueryNode的类型来决定。
综上所述,在这个部分中,通过vistor的模式,不停的自顶向下遍历AST上的节点,同时根据节点的类型来生成QT上的节点。
这样AST遍历完成之后,我们也得到了我们需要的QT,接下来就是这么使用QT的问题了。
我们来仔细审视一下QT的结构,如下图所示(该QT对应的xpath语句是://hello/test3
Java代码
[jcr:contains(@name, 'aa') and jcr:contains(@content, 'Maven') or @data1 = 'ahuaxuan'] order by jcr:score() descending, @name ascending
):
从上图看出,一条xpath语句最终被解析成一颗树,而这颗树代表着查询的条件,排序信息等等。
最重要的一个节点QueryRootNode,它连同它的children代表着一条xpath语句,这个时候,xpath语句的语义已经被清晰的划分开。接下来,就是QueryNode的Visitor上场了。我们来看看它的Visitor:
Java代码
public interface QueryNodeVisitor {
Object visit(QueryRootNode node, Object data);
Object visit(OrQueryNode node, Object data);
Object visit(AndQueryNode node, Object data);
Object visit(NotQueryNode node, Object data);
Object visit(ExactQueryNode node, Object data);
Object visit(NodeTypeQueryNode node, Object data);
Object visit(TextsearchQueryNode node, Object data);
Object visit(PathQueryNode node, Object data);
Object visit(LocationStepQueryNode node, Object data);
Object visit(RelationQueryNode node, Object data);
Object visit(OrderQueryNode node, Object data);
Object visit(DerefQueryNode node, Object data);
Object visit(PropertyFunctionQueryNode node, Object data);
}
它的实现类中需要定义每一种QueryNode的处理逻辑,显然和SimpleNode的XPathVisitor有一点不一样,因为在 XPathVisitor中,是通过switch来决定如何处理node的,而这里变成了多个方法来处理,看上去更加直观,只是如果再加一个 QueryNode的子类,那么接口就需要改动(从这一点来看vistor比较适合数据结构不易变更的场景,比如asm中,字节码结构一般是不会变更的,所以用visitor模式是比较适合的),不过总的来说,SimpleNode和XpathVisitor的关系就如同QueryNode和 QueryNodeVisitor一样,连续两个关键步骤都使用了visitor模式。
看过接口之后,就是看它的主实现类,LuceneQueryBuilder,这个类相当重要,所有的query对象就是在这个类中构造出来的,很显然,构造query对象所需要的条件就藏在QT中。我们只要稍稍的查看一个方法,就可以得到证据:
Java代码
public Object visit(QueryRootNode node, Object data) {
BooleanQuery root = new BooleanQuery();
Query wrapped = root;
if (node.getLocationNode() != null) {
wrapped = (Query) node.getLocationNode().accept(this, root);
}
return wrapped;
}
显然,从这段代码里,开始了便利QT的旅程,因为QueryRootNode是QT的root,所以这里也是自顶向下遍历,比如,在创建了root之后,接着去处理QT上的LocationNode(也就是PathQueryNode节点),于是方法就跳到了:
Java代码
public Object visit(PathQueryNode node, Object data) {
Query context = null;
LocationStepQueryNode[] steps = node.getPathSteps();
if (steps.length > 0) {
if (node.isAbsolute() && !steps[0].getIncludeDescendants()) {
// eat up first step
Name nameTest = steps[0].getNameTest();
if (nameTest == null) {
// this is equivalent to the root node
context = new TermQuery(new Term(FieldNames.PARENT, ""));
} else if (nameTest.getLocalName().length() == 0) {
// root node
context = new TermQuery(new Term(FieldNames.PARENT, ""));
} else {
// then this is a node != the root node
// will never match anything!
String name = "";
try {
name = resolver.getJCRName(nameTest);
} catch (NamespaceException e) {
exceptions.add(e);
}
BooleanQuery and = new BooleanQuery();
and.add(new TermQuery(new Term(FieldNames.PARENT, "")), Occur.MUST);
and.add(new TermQuery(new Term(FieldNames.LABEL, name)), Occur.MUST);
context = and;
}
LocationStepQueryNode[] tmp = new LocationStepQueryNode[steps.length - 1];
System.arraycopy(steps, 1, tmp, 0, steps.length - 1);
steps = tmp;
} else {
// path is 1) relative or 2) descendant-or-self
// use root node as context
context = new TermQuery(new Term(FieldNames.PARENT, ""));
}
} else {
exceptions.add(new InvalidQueryException("Number of location steps must be > 0"));
}
// loop over steps
for (int i = 0; i < steps.length; i++) {
context = (Query) steps[i].accept(this, context);
}
if (data instanceof BooleanQuery) {
BooleanQuery constraint = (BooleanQuery) data;
if (constraint.getClauses().length > 0) {
constraint.add(context, Occur.MUST);
context = constraint;
}
}
return context;
}
如此下去,整个QT会被遍历,并生成lucene的query对象,但是如果有同学也去读源码,那么可能会发现在处理Order这个语义时,visitor直接跳过了:
Java代码
public Object visit(OrderQueryNode node, Object data) {
return data;
}
没错,它是故意的,因为order的语义在jackrabbit里被独立拿来出来,生成需要排序的field。
生成lucene的query对象之后,就可以名正言顺的调用下面这段代码了:
searcher.search(query, new Sort(sortFields))
其中的sortFields是在queryimpl的execute方法里独立拿出来,并生成了sortfields:
Java代码
OrderQueryNode orderNode = root.getOrderNode();
OrderQueryNode.OrderSpec[] orderSpecs;
if (orderNode != null) {
orderSpecs = orderNode.getOrderSpecs();
} else {
orderSpecs = new OrderQueryNode.OrderSpec[0];
}
Name[] orderProperties = new Name[orderSpecs.length];
boolean[] ascSpecs = new boolean[orderSpecs.length];
for (int i = 0; i < orderSpecs.length; i++) {
orderProperties[i] = orderSpecs[i].getProperty();
ascSpecs[i] = orderSpecs[i].isAscending();
}
那么说到这里,jackrabbit在查询时候的一些关键流程就告一段落了,唯一不足的是本文只是描述了一条xpath语句从字符串到QT,再到lucene的query对象这样一个概要流程,在下一篇文章中,我们将会来着重看一下lucene是怎么使用这颗QT的。