/* * JBoss, Home of Professional Open Source. * Copyright 2012, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * 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.jboss.capedwarf.search; import java.util.Collections; import com.google.appengine.api.search.Field; import com.google.appengine.api.search.GeoPoint; import com.google.appengine.api.search.query.ParserUtils; import com.google.appengine.api.search.query.QueryLexer; import com.google.appengine.api.search.query.QueryTreeContext; import com.google.appengine.api.search.query.QueryTreeVisitor; import com.google.appengine.repackaged.org.antlr.runtime.tree.Tree; import org.apache.lucene.analysis.standard.StandardAnalyzer; import org.apache.lucene.index.Term; import org.apache.lucene.queryParser.ParseException; import org.apache.lucene.queryParser.QueryParser; import org.apache.lucene.search.BooleanClause; import org.apache.lucene.search.BooleanQuery; import org.apache.lucene.search.NumericRangeQuery; import org.apache.lucene.search.Query; import org.apache.lucene.search.TermQuery; import org.apache.lucene.search.TermRangeQuery; import org.apache.lucene.util.Version; import org.hibernate.search.spatial.SpatialQueryBuilder; /** * @author <a href="mailto:mluksa@redhat.com">Marko Luksa</a> */ public class GAEQueryTreeVisitor implements QueryTreeVisitor<Context> { public static final Version LUCENE_VERSION = Version.LUCENE_36; private String allFieldName; public GAEQueryTreeVisitor(String allFieldName) { this.allFieldName = allFieldName; } public void visitSequence(Tree tree, Context context) { visitConjunction(tree, context); // "author:bob author:alice" is equivalent to "author:bob AND author:alice" } public void visitConjunction(Tree tree, Context context) { BooleanQuery booleanQuery = new BooleanQuery(); walkThroughChildren(booleanQuery, BooleanClause.Occur.MUST, context); context.setQuery(booleanQuery); } public void visitDisjunction(Tree tree, Context context) { BooleanQuery booleanQuery = new BooleanQuery(); walkThroughChildren(booleanQuery, BooleanClause.Occur.SHOULD, context); context.setQuery(booleanQuery); } public void visitNegation(Tree tree, Context context) { BooleanQuery booleanQuery = new BooleanQuery(); booleanQuery.add(new TermQuery(new Term(CacheValue.MATCH_ALL_DOCS_FIELD_NAME, CacheValue.MATCH_ALL_DOCS_FIELD_VALUE)), BooleanClause.Occur.MUST); walkThroughChildren(booleanQuery, BooleanClause.Occur.MUST_NOT, context); context.setQuery(booleanQuery); } private void walkThroughChildren(BooleanQuery booleanQuery, BooleanClause.Occur occur, Context context) { for (Context childContext : context.children()) { booleanQuery.add(childContext.getQuery(), occur); } } public void visitFuzzy(Tree tree, Context context) { context.setRewriteMode(QueryTreeContext.RewriteMode.FUZZY); } public void visitLiteral(Tree tree, Context context) { context.setRewriteMode(QueryTreeContext.RewriteMode.STRICT); } public void visitLessThan(Tree tree, Context context) { visitOperator(context, Operator.LESS_THAN); } public void visitLessOrEqual(Tree tree, Context context) { visitOperator(context, Operator.LESS_OR_EQUAL); } public void visitGreaterThan(Tree tree, Context context) { visitOperator(context, Operator.GREATER_THAN); } public void visitGreaterOrEqual(Tree tree, Context context) { visitOperator(context, Operator.GREATER_OR_EQUAL); } public void visitEqual(Tree tree, Context context) { visitOperator(context, Operator.EQ); } public void visitContains(Tree tree, Context context) { visitOperator(context, Operator.CONTAINS); } protected void visitOperator(Context context, Operator operator) { context.setQuery(createComparisonQuery(context, operator)); } public void visitValue(Tree tree, Context context) { Tree type = tree.getChild(0); Tree value = tree.getChild(1); if (type.getType() == QueryLexer.STRING) { StringBuilder builder = new StringBuilder(); for (int i = 1; i < tree.getChildCount(); i++) builder.append(tree.getChild(i).getText()); context.setText(builder.toString()); context.setKind(QueryTreeContext.Kind.PHRASE); context.setReturnType(QueryTreeContext.Type.TEXT); } else if (type.getType() == QueryLexer.DIGIT) { // TODO -- OK? context.setKind(QueryTreeContext.Kind.LITERAL); context.setReturnType(QueryTreeContext.Type.NUMBER); } else { String text = value.getText(); context.setText(text); context.setKind(QueryTreeContext.Kind.LITERAL); context.setReturnType(ParserUtils.isNumber(text) ? QueryTreeContext.Type.NUMBER : QueryTreeContext.Type.TEXT); } } private Query createComparisonQuery(Context context, Operator operator) { Context leftSide = context.getChild(0); Context rightSide = context.getChild(1); if (leftSide.isFunction() || rightSide.isFunction()) { // TODO Context function; Context value; if (leftSide.isFunction()) { function = leftSide; value = rightSide; } else { value = leftSide; function = rightSide; } if (!"distance".equals(function.getText())) { throw new IllegalArgumentException("Unsupported function " + leftSide.getText()); } Context leftArgument = function.getChild(0); Context rightArgument = function.getChild(1); String fieldName = leftArgument.isCompatibleWith(QueryTreeContext.Type.LOCATION) ? rightArgument.getText() : leftArgument.getText(); GeoPoint geoPoint = leftArgument.isCompatibleWith(QueryTreeContext.Type.LOCATION) ? leftArgument.getGeoPoint() : rightArgument.getGeoPoint(); double latitude = geoPoint.getLatitude(); double longitude = geoPoint.getLongitude(); double radius = Double.parseDouble(value.getText()) / 1000; // need to convert from m to km String prefixedFieldName = new FieldNamePrefixer().getPrefixedFieldName(fieldName, Field.FieldType.GEO_POINT); return SpatialQueryBuilder.buildSpatialQueryByQuadTree(latitude, longitude, radius, prefixedFieldName); } else { return createQuery(leftSide.getText(), operator, rightSide); } } protected Query createQuery(String field, Operator operator, Context value) { if (value.isCompatibleWith(QueryTreeContext.Type.NUMBER)) { return createNumericQuery(field, operator, value); } else { return createTextQuery(field, operator, value); } } protected Query createNumericQuery(String field, Operator operator, Context value) { double doubleValue = Double.parseDouble(value.getText()); switch (operator) { case CONTAINS: BooleanQuery bool = new BooleanQuery(); bool.add(new BooleanClause(NumericRangeQuery.newDoubleRange(field, doubleValue, doubleValue, true, true), BooleanClause.Occur.SHOULD)); bool.add(new BooleanClause(new TermQuery(new Term(field, value.getText())), BooleanClause.Occur.SHOULD)); return bool; case EQ: return NumericRangeQuery.newDoubleRange(field, doubleValue, doubleValue, true, true); case GREATER_THAN: return NumericRangeQuery.newDoubleRange(field, doubleValue, null, false, false); case GREATER_OR_EQUAL: return NumericRangeQuery.newDoubleRange(field, doubleValue, null, true, true); case LESS_THAN: return NumericRangeQuery.newDoubleRange(field, null, doubleValue, false, false); case LESS_OR_EQUAL: return NumericRangeQuery.newDoubleRange(field, null, doubleValue, true, true); default: // fail fast throw new RuntimeException("Unsupported operator: " + operator); } } protected Query createTextQuery(String field, Operator operator, Context text) { switch (operator) { case CONTAINS: return createContainsQuery(field, text); case EQ: return new TermRangeQuery(field, text.getText(), text.getText(), true, true); case GREATER_THAN: return new TermRangeQuery(field, text.getText(), null, false, false); case GREATER_OR_EQUAL: return new TermRangeQuery(field, text.getText(), null, true, true); case LESS_THAN: return new TermRangeQuery(field, null, text.getText(), false, false); case LESS_OR_EQUAL: return new TermRangeQuery(field, null, text.getText(), true, true); default: // fail fast throw new RuntimeException("Unsupported operator: " + operator); } } private Query createContainsQuery(String field, Context text) { if (text.isPhrase()) { try { return new QueryParser(LUCENE_VERSION, null, new StandardAnalyzer(LUCENE_VERSION, Collections.emptySet())).parse(field + ":" + text.getText()); } catch (ParseException e) { throw new RuntimeException(e); } } else { return new TermQuery(new Term(field, text.getText().toLowerCase())); } } public void visitFunction(Tree tree, Context context) { String functionName = tree.getChild(0).getText(); if ("geopoint".equals(functionName)) { context.setReturnType(QueryTreeContext.Type.LOCATION); context.setKind(QueryTreeContext.Kind.FUNCTION); context.setGeoPoint(new GeoPoint(Double.valueOf(context.getChild(0).getText()), Double.valueOf(context.getChild(1).getText()))); context.setText("geopoint"); } else if ("distance".equals(functionName)) { context.setReturnType(QueryTreeContext.Type.NUMBER); context.setKind(QueryTreeContext.Kind.FUNCTION); context.setText(functionName); } // TODO } public void visitGlobal(Tree tree, Context context) { context.setText(allFieldName); } public void visitOther(Tree tree, Context context) { throw new RuntimeException("should never come here"); } }