/* * Copyright (C) 2010 eXo Platform SAS. * * This is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with this software; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF * site: http://www.fsf.org. */ package org.xcmis.search.lucene; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.antlr.runtime.ANTLRStringStream; import org.antlr.runtime.CharStream; import org.antlr.runtime.CommonTokenStream; import org.antlr.runtime.RecognitionException; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.Validate; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.document.DateTools; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.Term; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanClause.Occur; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.MatchAllDocsQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TermRangeQuery; import org.apache.lucene.search.WildcardQuery; import org.apache.lucene.search.regex.RegexQuery; import org.apache.lucene.util.NumericUtils; import org.apache.lucene.util.Version; import org.xcmis.search.InvalidQueryException; import org.xcmis.search.QueryObjectModelVisitor; import org.xcmis.search.VisitException; import org.xcmis.search.Visitors; import org.xcmis.search.antlr.FullTextLexer; import org.xcmis.search.antlr.FullTextParser; import org.xcmis.search.config.IndexConfiguration; import org.xcmis.search.lucene.content.ErrorReporterImpl; import org.xcmis.search.lucene.index.ExtendedNumberTools; import org.xcmis.search.lucene.index.FieldNames; import org.xcmis.search.lucene.index.IndexException; import org.xcmis.search.lucene.search.CaseInsensitiveRangeQuery; import org.xcmis.search.lucene.search.CaseInsensitiveRegexCapImpl; import org.xcmis.search.lucene.search.CaseInsensitiveTermQuery; import org.xcmis.search.lucene.search.ChildTraversingQueryNode; import org.xcmis.search.lucene.search.DescendantQueryNode; import org.xcmis.search.model.Limit; import org.xcmis.search.model.column.Column; import org.xcmis.search.model.constraint.And; import org.xcmis.search.model.constraint.ChildNode; import org.xcmis.search.model.constraint.Comparison; import org.xcmis.search.model.constraint.DescendantNode; import org.xcmis.search.model.constraint.FullTextSearch; import org.xcmis.search.model.constraint.Not; import org.xcmis.search.model.constraint.Operator; import org.xcmis.search.model.constraint.Or; import org.xcmis.search.model.constraint.PropertyExistence; import org.xcmis.search.model.constraint.SameNode; import org.xcmis.search.model.operand.BindVariableName; import org.xcmis.search.model.operand.FullTextSearchScore; import org.xcmis.search.model.operand.Length; import org.xcmis.search.model.operand.Literal; import org.xcmis.search.model.operand.LowerCase; import org.xcmis.search.model.operand.NodeDepth; import org.xcmis.search.model.operand.NodeLocalName; import org.xcmis.search.model.operand.NodeName; import org.xcmis.search.model.operand.PropertyValue; import org.xcmis.search.model.operand.UpperCase; import org.xcmis.search.model.ordering.Ordering; import org.xcmis.search.model.source.Join; import org.xcmis.search.model.source.Selector; import org.xcmis.search.model.source.join.ChildNodeJoinCondition; import org.xcmis.search.model.source.join.DescendantNodeJoinCondition; import org.xcmis.search.model.source.join.EquiJoinCondition; import org.xcmis.search.model.source.join.SameNodeJoinCondition; import org.xcmis.search.value.NameConverter; import org.xcmis.search.value.PathSplitter; /** * @author <a href="mailto:Sergey.Kabashnyuk@gmail.com">Sergey Kabashnyuk</a> * @version $Id: exo-jboss-codetemplates.xml 34360 2009-07-22 23:58:59Z * aheritier $ * */ public class LuceneQueryBuilder implements QueryObjectModelVisitor { public static final char LIKE_ESCAPE_CHAR = '\\'; public static final char LIKE_MATCH_ONE_CHAR = '_'; public static final char LIKE_MATCH_ZERO_OR_MORE_CHAR = '%'; private Stack<Object> queryBuilderStack; private Map<String, Object> bindVariablesValues; private final NameConverter nameConverter; private final PathSplitter pathSplitter; private final Pattern fullTextFieldNamePattern = Pattern.compile("^(.*:FULL|FULL):.*$"); /** * Lucene index reader. */ private IndexReader indexReader; private final IndexConfiguration indexConfiguration; /** * @param indexReader * @param nameConverter * @param pathSplitter * @param bindVariablesValues * @param indexConfiguration */ public LuceneQueryBuilder(IndexReader indexReader, NameConverter<?> nameConverter, PathSplitter<?> pathSplitter, Map<String, Object> bindVariablesValues, IndexConfiguration indexConfiguration) { this.indexConfiguration = indexConfiguration; Validate.notNull(indexReader, "The indexReader argument may not be null"); this.indexReader = indexReader; this.nameConverter = nameConverter; this.pathSplitter = pathSplitter; this.bindVariablesValues = bindVariablesValues; this.queryBuilderStack = new Stack<Object>(); } /** * * @return Return lucene query. */ public Query getQuery() { Query result = (Query)queryBuilderStack.pop(); this.queryBuilderStack = new Stack<Object>(); return result; } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.constraint.And) */ public void visit(And node) throws VisitException { // TODO check selector size; // final int selectors = node.getSelectorsNames().size(); final int selectors = 1; if (selectors == 1) { // Operators will push query to stack Visitors.visit(node.getLeft(), this); Visitors.visit(node.getRight(), this); BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add((Query)queryBuilderStack.pop(), BooleanClause.Occur.MUST); booleanQuery.add((Query)queryBuilderStack.pop(), BooleanClause.Occur.MUST); queryBuilderStack.push(booleanQuery); } else { //TODO check throw new UnsupportedOperationException("More then one selector used"); } } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.operand.BindVariableName) */ public void visit(BindVariableName node) throws VisitException { final Object variableValue = bindVariablesValues.get(nameConverter.convertName(node.getVariableName())); if (variableValue == null) { throw new VisitException("No value bound for " + node.getVariableName()); } queryBuilderStack.push(variableValue); } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.constraint.ChildNode) */ public void visit(ChildNode node) throws VisitException { String parentPath = node.getParentPath(); if (parentPath.charAt(0) == '[') { //uuid based absolute path Query parentQuery = new TermQuery(new Term(FieldNames.UUID, parentPath.substring(1, parentPath.length() - 1))); Query childNodeQuery = new ChildTraversingQueryNode(parentQuery, false); queryBuilderStack.push(childNodeQuery); } else { final Object[] entries = pathSplitter.splitPath(parentPath); if (entries.length > 0) { Query childNodeQuery = null; for (int i = 0; i < entries.length; i++) { if (i == 0) { childNodeQuery = new TermQuery(new Term(FieldNames.UUID, indexConfiguration.getRootUuid())); } else { final String stepName = nameConverter.convertName(entries[i]); final Query nameQuery = new TermQuery(new Term(FieldNames.LABEL, stepName)); childNodeQuery = new DescendantQueryNode(nameQuery, childNodeQuery); } } // all child childNodeQuery = new ChildTraversingQueryNode(childNodeQuery, false); queryBuilderStack.push(childNodeQuery); } else { queryBuilderStack.push(new MatchAllDocsQuery()); } } } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.source.join.ChildNodeJoinCondition) */ public void visit(ChildNodeJoinCondition node) throws VisitException { } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.column.Column) */ public void visit(Column node) throws VisitException { } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.constraint.Comparison) */ public void visit(Comparison node) throws VisitException { // Push static value to stack Visitors.visit(node.getOperand2(), this); // push operator to stack queryBuilderStack.push(node.getOperator()); // push ignore case flag queryBuilderStack.push(new Boolean(false)); // Push query from DynamicOperand to stack Visitors.visit(node.getOperand1(), this); } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.constraint.DescendantNode) */ public void visit(DescendantNode node) throws VisitException { String parentPath = node.getAncestorPath(); if (parentPath.charAt(0) == '[') { //uuid based absolute path Query parentQuery = new TermQuery(new Term(FieldNames.UUID, parentPath.substring(1, parentPath.length() - 1))); Query childNodeQuery = new ChildTraversingQueryNode(parentQuery, true); queryBuilderStack.push(childNodeQuery); } else { final Object[] entries = pathSplitter.splitPath(parentPath); Query descendantQuery = new TermQuery(new Term(FieldNames.UUID, indexConfiguration.getRootUuid())); for (int i = 1; i < entries.length; i++) { final String stepName = nameConverter.convertName(entries[i]); final Query nameQuery = new TermQuery(new Term(FieldNames.LABEL, stepName)); descendantQuery = new DescendantQueryNode(nameQuery, descendantQuery); } // all childs descendantQuery = new ChildTraversingQueryNode(descendantQuery, true); queryBuilderStack.push(descendantQuery); } } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.source.join.DescendantNodeJoinCondition) */ public void visit(DescendantNodeJoinCondition node) throws VisitException { } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.source.join.EquiJoinCondition) */ public void visit(EquiJoinCondition node) throws VisitException { } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.constraint.FullTextSearch) */ public void visit(FullTextSearch node) throws VisitException { // TODO selector unused, research it // TODO remove to string final CharStream input = new ANTLRStringStream(node.getFullTextSearchExpression().toString()); final FullTextLexer lexer = new FullTextLexer(input); final CommonTokenStream tokens = new CommonTokenStream(lexer); final FullTextParser parser = new FullTextParser(tokens); final ErrorReporterImpl reporter = new ErrorReporterImpl(); lexer.setErrorReporter(reporter); parser.setErrorReporter(reporter); final List<String> fields = new ArrayList<String>(); // search by specific field if (node.getPropertyName() != null) { fields.add(FieldNames.createFullTextFieldName(node.getPropertyName())); } else { // search by all full text fields Set<String> names; try { names = getFieldNames(); } catch (IndexException e) { throw new VisitException(e.getLocalizedMessage()); } for (final String fieldName : names) { final Matcher matcher = fullTextFieldNamePattern.matcher(fieldName); if (matcher.matches()) { fields.add(fieldName); } } } Query query = null; try { parser.fulltext(fields, new StandardAnalyzer(Version.LUCENE_35)); query = parser.getQuery(); final InvalidQueryException ex = reporter.getException(); if (ex != null) { throw new VisitException(ex.getLocalizedMessage()); } } catch (final RecognitionException e) { throw new VisitException(e.getMessage()); } queryBuilderStack.push(query); } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.operand.FullTextSearchScore) */ public void visit(FullTextSearchScore node) throws VisitException { throw new UnsupportedOperationException("FulltextSearchScore is unsupported operation."); } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.source.Join) */ public void visit(Join node) throws VisitException { } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.operand.Length) */ public void visit(Length node) throws VisitException { Validate.isTrue(queryBuilderStack.peek() instanceof Boolean, "Stack should contains caseInsensitiveSearch flag"); boolean caseInsensitiveSearch = (Boolean)queryBuilderStack.pop(); Validate.isTrue(queryBuilderStack.peek() instanceof Operator, "Stack should contains comparation operator "); Operator operator = (Operator)queryBuilderStack.pop(); Validate.isTrue(queryBuilderStack.peek() instanceof Long, "Invalid literal type, should be long. But found " + queryBuilderStack.peek().getClass().getCanonicalName()); Long staticLongValue = (Long)queryBuilderStack.pop(); String value = NumericUtils.longToPrefixCoded(staticLongValue); String propertyField = FieldNames.createFieldLengthName(node.getPropertyValue().getPropertyName()); Term lengthTerm = new Term(propertyField, value); switch (operator) { case EQUAL_TO : queryBuilderStack.push(new TermQuery(lengthTerm)); break; case NOT_EQUAL_TO : final BooleanQuery booleanQuery = new BooleanQuery(); // property exists booleanQuery.add(new TermQuery(new Term(FieldNames.PROPERTIES_SET, node.getPropertyValue() .getPropertyName())), BooleanClause.Occur.SHOULD); booleanQuery.add(new TermQuery(lengthTerm), BooleanClause.Occur.MUST_NOT); queryBuilderStack.push(booleanQuery); break; case GREATER_THAN : queryBuilderStack.push(new TermRangeQuery(lengthTerm.field(), lengthTerm.text(), null, false, false)); break; case GREATER_THAN_OR_EQUAL_TO : queryBuilderStack.push(new TermRangeQuery(lengthTerm.field(), lengthTerm.text(), null, true, true)); break; case LESS_THAN : queryBuilderStack.push(new TermRangeQuery(lengthTerm.field(), null, lengthTerm.text(), false, false)); break; case LESS_THAN_OR_EQUAL_TO : queryBuilderStack.push(new TermRangeQuery(lengthTerm.field(), null, lengthTerm.text(), true, true)); break; case LIKE : throw new VisitException("Unsupported operation for Length operator"); default : throw new VisitException("Invalid operator " + operator); } } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.Limit) */ public void visit(Limit limit) throws VisitException { } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.operand.Literal) */ public void visit(Literal node) throws VisitException { queryBuilderStack.push(node.getValue()); } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.operand.LowerCase) */ public void visit(LowerCase node) throws VisitException { Validate.isTrue(queryBuilderStack.peek() instanceof Boolean, "Stack should contains caseInsensitiveSearch flag"); boolean caseInsensitiveSearch = (Boolean)queryBuilderStack.pop(); final String value = (String)queryBuilderStack.peek(); if (!caseInsensitiveSearch && !StringUtils.isAllLowerCase(value)) { // search nothing because static value in different case queryBuilderStack.push(new BooleanQuery()); } queryBuilderStack.push(new Boolean(true)); // push dynamic query to stack; Visitors.visit(node.getOperand(), this); } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.operand.NodeDepth) */ public void visit(NodeDepth depth) throws VisitException { } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.operand.NodeLocalName) */ public void visit(NodeLocalName node) throws VisitException { Validate.isTrue(queryBuilderStack.peek() instanceof Boolean, "Stack should contains caseInsensitiveSearch flag"); boolean caseInsensitiveSearch = (Boolean)queryBuilderStack.pop(); Validate.isTrue(queryBuilderStack.peek() instanceof Operator, "Stack should contains comparation operator "); Operator operator = (Operator)queryBuilderStack.pop(); Validate.isTrue(queryBuilderStack.peek() instanceof String, "Stack should contains static value. But found " + queryBuilderStack.peek().getClass().getCanonicalName()); String staticStingValue = (String)queryBuilderStack.pop(); Term staticValueTerm = new Term(FieldNames.LABEL, staticStingValue); switch (operator) { case EQUAL_TO : if (caseInsensitiveSearch) { throw new VisitException("Unsupported operation of caseinsensetive search and NodeLocalName"); } final BooleanQuery equalToQuery = new BooleanQuery(); equalToQuery.add(new WildcardQuery(new Term(FieldNames.LABEL, "*?:" + staticStingValue)), BooleanClause.Occur.SHOULD); equalToQuery.add(new TermQuery(staticValueTerm), BooleanClause.Occur.SHOULD); queryBuilderStack.push(equalToQuery); break; case NOT_EQUAL_TO : if (caseInsensitiveSearch) { throw new VisitException("Unsupported operation of caseinsensetive search and NodeLocalName"); } final BooleanQuery notEqualToQuery = new BooleanQuery(); // property exists notEqualToQuery.add(new MatchAllDocsQuery(), BooleanClause.Occur.SHOULD); final BooleanQuery q = new BooleanQuery(); q.add(new WildcardQuery(new Term(FieldNames.LABEL, "*?:" + staticStingValue)), BooleanClause.Occur.SHOULD); q.add(new TermQuery(new Term(FieldNames.LABEL, staticStingValue)), BooleanClause.Occur.SHOULD); notEqualToQuery.add(q, BooleanClause.Occur.MUST_NOT); queryBuilderStack.push(notEqualToQuery); break; case GREATER_THAN : case GREATER_THAN_OR_EQUAL_TO : case LESS_THAN : case LESS_THAN_OR_EQUAL_TO : throw new VisitException("Unsupported comparation :" + operator.toString() + " and NodeLocalName"); case LIKE : final String likeExpression = staticStingValue; Query likeQuery = null; if (likeExpression.equals("%")) { // property exists likeQuery = new MatchAllDocsQuery(); } else { final String term = "(.+:)?" + likePatternToRegex(likeExpression); likeQuery = new RegexQuery(new Term(FieldNames.LABEL, term)); if (caseInsensitiveSearch) { ((RegexQuery)likeQuery).setRegexImplementation(new CaseInsensitiveRegexCapImpl()); } } queryBuilderStack.push(likeQuery); break; default : throw new VisitException("Invalid operator " + operator); } } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.operand.NodeName) */ public void visit(NodeName node) throws VisitException { Validate.isTrue(queryBuilderStack.peek() instanceof Boolean, "Stack should contains caseInsensitiveSearch flag"); boolean caseInsensitiveSearch = (Boolean)queryBuilderStack.pop(); Validate.isTrue(queryBuilderStack.peek() instanceof Operator, "Stack should contains comparation operator "); Operator operator = (Operator)queryBuilderStack.pop(); Validate.isTrue(queryBuilderStack.peek() instanceof String, "Stack should contains static value. But found " + queryBuilderStack.peek().getClass().getCanonicalName()); String staticStingValue = (String)queryBuilderStack.pop(); Term staticValueTerm = new Term(FieldNames.LABEL, staticStingValue); switch (operator) { case EQUAL_TO : if (caseInsensitiveSearch) { queryBuilderStack.push(new CaseInsensitiveTermQuery(new Term(FieldNames.LABEL, staticStingValue .toLowerCase()))); } else { queryBuilderStack.push(new TermQuery(staticValueTerm)); } break; case NOT_EQUAL_TO : final BooleanQuery booleanQuery = new BooleanQuery(); // property exists booleanQuery.add(new MatchAllDocsQuery(), BooleanClause.Occur.SHOULD); // property not equal to if (caseInsensitiveSearch) { booleanQuery.add(new CaseInsensitiveTermQuery(staticValueTerm), BooleanClause.Occur.MUST_NOT); } else { booleanQuery.add(new TermQuery(staticValueTerm), BooleanClause.Occur.MUST_NOT); } queryBuilderStack.push(booleanQuery); break; case GREATER_THAN : if (caseInsensitiveSearch) { queryBuilderStack.push(new CaseInsensitiveRangeQuery(staticValueTerm.field(), staticValueTerm.text().toLowerCase(), null, false, false)); } else { queryBuilderStack.push(new TermRangeQuery(staticValueTerm.field(), staticValueTerm.text().toLowerCase(), null, false, false)); } break; case GREATER_THAN_OR_EQUAL_TO : if (caseInsensitiveSearch) { queryBuilderStack.push(new CaseInsensitiveRangeQuery(staticValueTerm.field(), staticValueTerm.text().toLowerCase(), null, true, true)); } else { queryBuilderStack.push(new TermRangeQuery(staticValueTerm.field(), staticValueTerm.text().toLowerCase(), null, true, true)); } break; case LESS_THAN : if (caseInsensitiveSearch) { queryBuilderStack.push(new CaseInsensitiveRangeQuery(staticValueTerm.field(), null, staticValueTerm.text().toUpperCase(), false, false)); } else { queryBuilderStack.push(new TermRangeQuery(staticValueTerm.field(), null, staticValueTerm.text().toUpperCase(), false, false)); } break; case LESS_THAN_OR_EQUAL_TO : if (caseInsensitiveSearch) { queryBuilderStack.push(new CaseInsensitiveRangeQuery(staticValueTerm.field(), null, staticValueTerm.text().toUpperCase(), true, true)); } else { queryBuilderStack.push(new TermRangeQuery(staticValueTerm.field(), null, staticValueTerm.text().toUpperCase(), true, true)); } break; case LIKE : final String likeExpression = staticStingValue; if (likeExpression.equals("%")) { // property exists queryBuilderStack.push(new MatchAllDocsQuery()); } else { final String term = likePatternToRegex(likeExpression); RegexQuery query = new RegexQuery(new Term(FieldNames.LABEL, term)); if (caseInsensitiveSearch) { (query).setRegexImplementation(new CaseInsensitiveRegexCapImpl()); } queryBuilderStack.push(query); } break; default : throw new VisitException("Invalid operator " + operator); } } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.constraint.Not) */ public void visit(Not node) throws VisitException { // Push query from Constraint to stack Visitors.visit(node.getConstraint(), this); final BooleanQuery resultQuery = new BooleanQuery(); // get query builded by Constraint. resultQuery.add((Query)queryBuilderStack.pop(), Occur.MUST_NOT); // combine with previous if (queryBuilderStack.size() > 0) { resultQuery.add((Query)queryBuilderStack.pop(), Occur.MUST); } else { // TODO optimize by adding initial query resultQuery.add(new MatchAllDocsQuery(), Occur.MUST); } queryBuilderStack.push(resultQuery); } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.constraint.Or) */ public void visit(Or node) throws VisitException { // Push query from left constraint to stack Visitors.visit(node.getLeft(), this); // Push query from right constraint to stack Visitors.visit(node.getRight(), this); final BooleanQuery resultQuery = new BooleanQuery(); // get query builded by left constraint. resultQuery.add((Query)queryBuilderStack.pop(), BooleanClause.Occur.SHOULD); // get query builded by right constraint. resultQuery.add((Query)queryBuilderStack.pop(), BooleanClause.Occur.SHOULD); queryBuilderStack.push(resultQuery); } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.ordering.Ordering) */ public void visit(Ordering node) throws VisitException { } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.constraint.PropertyExistence) */ public void visit(PropertyExistence node) throws VisitException { queryBuilderStack.push(new TermQuery(new Term(FieldNames.PROPERTIES_SET, node.getPropertyName()))); } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.operand.PropertyValue) */ public void visit(PropertyValue node) throws VisitException { Validate.isTrue(queryBuilderStack.peek() instanceof Boolean, "Stack should contains caseInsensitiveSearch flag"); boolean caseInsensitiveSearch = (Boolean)queryBuilderStack.pop(); Validate.isTrue(queryBuilderStack.peek() instanceof Operator, "Stack should contains comparation operator "); Operator operator = (Operator)queryBuilderStack.pop(); Object staticValue = queryBuilderStack.peek(); Validate.isTrue((staticValue instanceof String || staticValue instanceof Double || staticValue instanceof Long || staticValue instanceof Calendar || staticValue instanceof Boolean), "Stack should contains static value. But found " + queryBuilderStack.peek().getClass().getCanonicalName()); staticValue = queryBuilderStack.pop(); String staticStingValue = null; //convert static value to string //TODO check cast system if (staticValue instanceof String) { staticStingValue = (String)staticValue; } else if (staticValue instanceof Double) { staticStingValue = ExtendedNumberTools.doubleToString((Double)staticValue); } else if (staticValue instanceof Long) { staticStingValue = NumericUtils.longToPrefixCoded((Long)staticValue); } else if (staticValue instanceof Calendar) { staticStingValue = DateTools.dateToString(((Calendar)staticValue).getTime(), DateTools.Resolution.MILLISECOND); } else if (staticValue instanceof Boolean) { staticStingValue = staticValue.toString(); } Term propertyValueTerm = new Term(FieldNames.createPropertyFieldName(node.getPropertyName()), staticStingValue); TermQuery propertyValueQuery = new TermQuery(propertyValueTerm); Term maxFildValue = new Term(FieldNames.createPropertyFieldName(node.getPropertyName()), "\uFFFF"); switch (operator) { case EQUAL_TO : if (caseInsensitiveSearch) { queryBuilderStack.push(new CaseInsensitiveTermQuery(propertyValueTerm)); } else { queryBuilderStack.push(propertyValueQuery); } break; case NOT_EQUAL_TO : final BooleanQuery notEqualQuery = new BooleanQuery(); // property exists notEqualQuery.add(new TermQuery(new Term(FieldNames.PROPERTIES_SET, node.getPropertyName())), BooleanClause.Occur.SHOULD); // property not equal to if (caseInsensitiveSearch) { notEqualQuery.add(new CaseInsensitiveTermQuery(propertyValueTerm), BooleanClause.Occur.MUST_NOT); } else { notEqualQuery.add(propertyValueQuery, BooleanClause.Occur.MUST_NOT); } queryBuilderStack.push(notEqualQuery); break; case GREATER_THAN : if (caseInsensitiveSearch) { queryBuilderStack.push(new CaseInsensitiveRangeQuery(FieldNames.createPropertyFieldName(node.getPropertyName()), propertyValueTerm.text(), maxFildValue.text(), false, false)); } else { queryBuilderStack.push(new TermRangeQuery(FieldNames.createPropertyFieldName(node.getPropertyName()), propertyValueTerm.text(), maxFildValue.text(), false, false)); } break; case GREATER_THAN_OR_EQUAL_TO : if (caseInsensitiveSearch) { queryBuilderStack.push(new CaseInsensitiveRangeQuery(FieldNames.createPropertyFieldName(node.getPropertyName()), propertyValueTerm.text(), maxFildValue.text(), true, true)); } else { queryBuilderStack.push(new TermRangeQuery(FieldNames.createPropertyFieldName(node.getPropertyName()), propertyValueTerm.text(), maxFildValue.text(), true, true)); } break; case LESS_THAN : if (caseInsensitiveSearch) { queryBuilderStack.push(new CaseInsensitiveRangeQuery(FieldNames.createPropertyFieldName(node.getPropertyName()), "", propertyValueTerm.text(), false, false)); } else { queryBuilderStack.push(new TermRangeQuery(FieldNames.createPropertyFieldName(node.getPropertyName()), "", propertyValueTerm.text(), false, false)); } break; case LESS_THAN_OR_EQUAL_TO : if (caseInsensitiveSearch) { queryBuilderStack.push(new CaseInsensitiveRangeQuery(FieldNames.createPropertyFieldName(node.getPropertyName()), "", propertyValueTerm.text(), true, true)); } else { queryBuilderStack.push(new TermRangeQuery(FieldNames.createPropertyFieldName(node.getPropertyName()), "", propertyValueTerm.text(), true, true)); } break; case LIKE : if (staticStingValue.equals("%")) { // property exists queryBuilderStack.push(new TermQuery(new Term(FieldNames.PROPERTIES_SET, node.getPropertyName()))); } else { final String term = likePatternToRegex(staticStingValue); Query likeQuery = new RegexQuery(new Term(FieldNames.createPropertyFieldName(node.getPropertyName()), term)); if (caseInsensitiveSearch) { ((RegexQuery)likeQuery).setRegexImplementation(new CaseInsensitiveRegexCapImpl()); } queryBuilderStack.push(likeQuery); } break; default : throw new VisitException("Invalid operator " + operator); } } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.Query) */ public void visit(org.xcmis.search.model.Query node) throws VisitException { } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.constraint.SameNode) */ public void visit(SameNode node) throws VisitException { final Object[] entries = pathSplitter.splitPath(node.getPath()); Query descendantQuery = null; for (int i = 0; i < entries.length; i++) { if (i == 0) { descendantQuery = new TermQuery(new Term(FieldNames.UUID, indexConfiguration.getRootUuid())); } else { final String stepName = nameConverter.convertName(entries[i]); final Query nameQuery = new TermQuery(new Term(FieldNames.LABEL, stepName)); descendantQuery = new DescendantQueryNode(nameQuery, descendantQuery); } } queryBuilderStack.push(descendantQuery); } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.source.join.SameNodeJoinCondition) */ public void visit(SameNodeJoinCondition node) throws VisitException { } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.source.Selector) */ public void visit(Selector selector) throws VisitException { } /** * @see org.xcmis.search.QueryObjectModelVisitor#visit(org.xcmis.search.model.operand.UpperCase) */ public void visit(UpperCase node) throws VisitException { Validate.isTrue(queryBuilderStack.peek() instanceof Boolean, "Stack should contains caseInsensitiveSearch flag"); boolean caseInsensitiveSearch = (Boolean)queryBuilderStack.pop(); final String value = (String)queryBuilderStack.peek(); if (!caseInsensitiveSearch && !StringUtils.isAllUpperCase(value)) { // search nothing because static value in different case queryBuilderStack.push(new BooleanQuery()); } queryBuilderStack.push(new Boolean(true)); // push dynamic query to stack; Visitors.visit(node.getOperand(), this); } /** * {@inheritDoc} */ private Set<String> getFieldNames() throws IndexException { final Set<String> fildsSet = new HashSet<String>(); @SuppressWarnings("unchecked") final Collection fields = indexReader.getFieldNames(IndexReader.FieldOption.ALL); for (final Object field : fields) { fildsSet.add((String)field); } return fildsSet; } /** * Transform Like pattern to regular expression. * * @param pattern Like pattern * @return String regular expression */ private String likePatternToRegex(final String pattern) { // - escape all non alphabetic characters // - escape constructs like \<alphabetic char> into \\<alphabetic char> // - replace non escaped _ % into . and .* final StringBuffer regexp = new StringBuffer(); regexp.append("^"); boolean escaped = false; for (int i = 0; i < pattern.length(); i++) { if (pattern.charAt(i) == LIKE_ESCAPE_CHAR) { if (escaped) { regexp.append("\\\\"); escaped = false; } else { escaped = true; } } else { if (Character.isLetterOrDigit(pattern.charAt(i))) { if (escaped) { regexp.append(pattern.charAt(i)); // append("\\\\") escaped = false; } else { regexp.append(pattern.charAt(i)); } } else { if (escaped) { regexp.append('\\').append(pattern.charAt(i)); escaped = false; } else { switch (pattern.charAt(i)) { case LIKE_MATCH_ONE_CHAR : regexp.append('.'); break; case LIKE_MATCH_ZERO_OR_MORE_CHAR : regexp.append(".*"); break; default : regexp.append('\\').append(pattern.charAt(i)); } } } } } regexp.append("$"); return regexp.toString(); } }