From 4922fd479c6cda26e1919d984910d514ad0cafe4 Mon Sep 17 00:00:00 2001 From: Robert Newson Date: Sat, 6 May 2023 09:01:30 +0100 Subject: WIP extend syntax parser for geo --- nouveau/pom.xml | 12 + nouveau/src/main/javacc/NouveauSyntaxParser.jj | 878 +++++++++++++++++++++++++ 2 files changed, 890 insertions(+) create mode 100644 nouveau/src/main/javacc/NouveauSyntaxParser.jj diff --git a/nouveau/pom.xml b/nouveau/pom.xml index 1c2466761..e2859633e 100644 --- a/nouveau/pom.xml +++ b/nouveau/pom.xml @@ -272,6 +272,18 @@ + + org.codehaus.mojo + javacc-maven-plugin + 3.0.1 + + + + javacc + + + + diff --git a/nouveau/src/main/javacc/NouveauSyntaxParser.jj b/nouveau/src/main/javacc/NouveauSyntaxParser.jj new file mode 100644 index 000000000..7389ce659 --- /dev/null +++ b/nouveau/src/main/javacc/NouveauSyntaxParser.jj @@ -0,0 +1,878 @@ +options { + STATIC = false; + JAVA_UNICODE_ESCAPE = true; + USER_CHAR_STREAM = true; + IGNORE_CASE = false; + JDK_VERSION = "1.8"; + + // FORCE_LA_CHECK = true; + // DEBUG_LOOKAHEAD = true; + // DEBUG_PARSER = true; +} + +PARSER_BEGIN(StandardSyntaxParser) +package org.apache.lucene.queryparser.flexible.standard.parser; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.StringReader; +import java.io.Reader; +import java.util.Collections; +import java.util.ArrayList; + +import org.apache.lucene.queryparser.flexible.core.nodes.AndQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.BooleanQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.BoostQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.FieldQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.FuzzyQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.GroupQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.ModifierQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.OrQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.QueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.QuotedFieldQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.SlopQueryNode; +import org.apache.lucene.queryparser.flexible.messages.Message; +import org.apache.lucene.queryparser.flexible.messages.MessageImpl; +import org.apache.lucene.queryparser.flexible.core.QueryNodeParseException; +import org.apache.lucene.queryparser.flexible.core.messages.QueryParserMessages; +import org.apache.lucene.queryparser.flexible.core.parser.SyntaxParser; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.After; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.AnalyzedText; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.AtLeast; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.Before; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.ContainedBy; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.Containing; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.Extend; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.FuzzyTerm; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.IntervalFunction; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.MaxGaps; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.MaxWidth; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.NonOverlapping; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.NotContainedBy; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.NotContaining; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.NotWithin; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.Or; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.Ordered; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.Overlapping; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.Phrase; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.Unordered; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.UnorderedNoOverlaps; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.Wildcard; +import org.apache.lucene.queryparser.flexible.standard.nodes.intervalfn.Within; +import org.apache.lucene.queryparser.flexible.standard.nodes.IntervalQueryNode; +import org.apache.lucene.queryparser.flexible.standard.nodes.MinShouldMatchNode; +import org.apache.lucene.queryparser.flexible.standard.nodes.RegexpQueryNode; +import org.apache.lucene.queryparser.charstream.CharStream; +import org.apache.lucene.queryparser.charstream.FastCharStream; +import org.apache.lucene.queryparser.flexible.standard.nodes.TermRangeQueryNode; + +import static org.apache.lucene.queryparser.flexible.standard.parser.EscapeQuerySyntaxImpl.discardEscapeChar; + +/** + * Parser for the standard Lucene syntax + */ +public class StandardSyntaxParser implements SyntaxParser { + public StandardSyntaxParser() { + this(new FastCharStream(Reader.nullReader())); + } + + /** + * Parses a query string, returning a {@link org.apache.lucene.queryparser.flexible.core.nodes.QueryNode}. + * @param query the query string to be parsed. + * @throws ParseException if the parsing fails + */ + @Override + public QueryNode parse(CharSequence query, CharSequence field) throws QueryNodeParseException { + ReInit(new FastCharStream(new StringReader(query.toString()))); + try { + return TopLevelQuery(field); + } catch (ParseException tme) { + tme.setQuery(query); + throw tme; + } catch (Error tme) { + Message message = new MessageImpl(QueryParserMessages.INVALID_SYNTAX_CANNOT_PARSE, query, tme.getMessage()); + QueryNodeParseException e = new QueryNodeParseException(tme); + e.setQuery(query); + e.setNonLocalizedMessage(message); + throw e; + } + } + + public static float parseFloat(Token token) { + return Float.parseFloat(token.image); + } + + public static int parseInt(Token token) { + return Integer.parseInt(token.image); + } +} +PARSER_END(StandardSyntaxParser) + +// Token definitions. + +<*> TOKEN : { + <#_NUM_CHAR: ["0"-"9"] > + // Every character that follows a backslash is considered as an escaped character + | <#_ESCAPED_CHAR: "\\" ~[] > + | <#_TERM_START_CHAR: ( ~[ " ", "\t", "\n", "\r", "\u3000", "+", "-", "!", "(", ")", ":", "^", "@", + "<", ">", "=", "[", "]", "\"", "{", "}", "~", "\\", "/"] + | <_ESCAPED_CHAR> ) > + | <#_TERM_CHAR: ( <_TERM_START_CHAR> | <_ESCAPED_CHAR> | "-" | "+" ) > + | <#_WHITESPACE: ( " " | "\t" | "\n" | "\r" | "\u3000") > + | <#_QUOTED_CHAR: ( ~[ "\"", "\\" ] | <_ESCAPED_CHAR> ) > +} + + SKIP : { + < <_WHITESPACE> > +} + + TOKEN : { + + | + | + | : Function + | + | + | + | + | + | " > + | =" > + | + | + | )* "\""> + | )+ ( "." (<_NUM_CHAR>)+ )? > + | (<_TERM_CHAR>)* > + | + | : Range + | : Range +} + + TOKEN : { + : DEFAULT +} + + TOKEN : { + + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | + | +} + + TOKEN : { + + | : DEFAULT + | : DEFAULT + | + | +} + + + +// Non-terminal production rules. + +/** + * The top-level rule ensures that there is no garbage after the query string. + * + *
{@code
+ * TopLevelQuery ::= Query 
+ * }
+ */ +public QueryNode TopLevelQuery(CharSequence field) : +{ + QueryNode q; +} +{ + q = Query(field) { + return q; + } +} + +/** + * A query consists of one or more disjunction queries (solves operator precedence). + *
{@code
+ * Query ::= DisjQuery ( DisjQuery )*
+ * DisjQuery ::= ConjQuery ( OR ConjQuery )*
+ * ConjQuery ::= ModClause ( AND ModClause )*
+ * }
+ */ +private QueryNode Query(CharSequence field) : { + ArrayList clauses = new ArrayList(); + QueryNode node; +} +{ + ( node = DisjQuery(field) { clauses.add(node); } )+ + { + // Handle the case of a "pure" negation query which + // needs to be wrapped as a boolean query, otherwise + // the returned result drops the negation. + if (clauses.size() == 1) { + QueryNode first = clauses.get(0); + if (first instanceof ModifierQueryNode + && ((ModifierQueryNode) first).getModifier() == ModifierQueryNode.Modifier.MOD_NOT) { + clauses.set(0, new BooleanQueryNode(Collections.singletonList(first))); + } + } + + return clauses.size() == 1 ? clauses.get(0) : new BooleanQueryNode(clauses); + } +} + +/** + * A disjoint clause consists of one or more conjunction clauses. + *
{@code
+ * DisjQuery ::= ConjQuery ( OR ConjQuery )*
+ * }
+ */ +private QueryNode DisjQuery(CharSequence field) : { + ArrayList clauses = new ArrayList(); + QueryNode node; +} +{ + node = ConjQuery(field) { clauses.add(node); } + ( node = ConjQuery(field) { clauses.add(node); } )* + { + return clauses.size() == 1 ? clauses.get(0) : new OrQueryNode(clauses); + } +} + +/** + * A conjunction clause consists of one or more modifier-clause pairs. + *
{@code
+ * ConjQuery ::= ModClause ( AND ModClause )*
+ * }
+ */ +private QueryNode ConjQuery(CharSequence field) : { + ArrayList clauses = new ArrayList(); + QueryNode node; +} +{ + node = ModClause(field) { clauses.add(node); } + ( node = ModClause(field) { clauses.add(node); } )* + { + return clauses.size() == 1 ? clauses.get(0) : new AndQueryNode(clauses); + } +} + +/** + * A modifier-atomic clause pair. + *
{@code
+ * ModClause ::= (Modifier)? Clause
+ * }
+ */ +private QueryNode ModClause(CharSequence field) : { + QueryNode q; + ModifierQueryNode.Modifier modifier = ModifierQueryNode.Modifier.MOD_NONE; +} +{ + ( { modifier = ModifierQueryNode.Modifier.MOD_REQ; } + | ( | ) { modifier = ModifierQueryNode.Modifier.MOD_NOT; } + )? + q = Clause(field) + { + if (modifier != ModifierQueryNode.Modifier.MOD_NONE) { + q = new ModifierQueryNode(q, modifier); + } + return q; + } +} + +/** + * An atomic clause consists of a field range expression, a potentially + * field-qualified term or a group. + * + *
{@code
+ * Clause ::= FieldRangeExpr
+ *          | (FieldName (':' | '='))? (Term | GroupingExpr)
+ * }
+ */ +private QueryNode Clause(CharSequence field) : { + QueryNode q; +} +{ + ( + LOOKAHEAD(2) q = FieldRangeExpr(field) + | (LOOKAHEAD(2) field = FieldName() ( | ))? + (LOOKAHEAD(2) q = Term(field) | q = GroupingExpr(field) | q = IntervalExpr(field)) + ) + { + return q; + } +} + +/** + * A field name. This utility method strips escape characters from field names. + */ +private CharSequence FieldName() : { + Token name; +} +{ + name = { return discardEscapeChar(name.image); } +} + +/** + * An grouping expression is a Query with potential boost applied to it. + * + *
{@code
+ * GroupingExpr ::= '(' Query ')' ('^' )?
+ * }
+ */ +private QueryNode GroupingExpr(CharSequence field) : { + QueryNode q; + Token boost, minShouldMatch = null; +} +{ + q = Query(field) (q = Boost(q))? ("@" minShouldMatch = )? + { + if (minShouldMatch != null) { + q = new MinShouldMatchNode(parseInt(minShouldMatch), new GroupQueryNode(q)); + } else { + q = new GroupQueryNode(q); + } + return q; + } +} + + +/** + * An interval expression (functions) node. + */ +private IntervalQueryNode IntervalExpr(CharSequence field) : { + IntervalFunction source; +} + { + source = IntervalFun() + { + return new IntervalQueryNode(field == null ? null : field.toString(), source); + } + } + +private IntervalFunction IntervalFun() : { + IntervalFunction source; +} +{ + LOOKAHEAD(2) source = IntervalAtLeast() { return source; } + | LOOKAHEAD(2) source = IntervalMaxWidth() { return source; } + | LOOKAHEAD(2) source = IntervalMaxGaps() { return source; } + | LOOKAHEAD(2) source = IntervalOrdered() { return source; } + | LOOKAHEAD(2) source = IntervalUnordered() { return source; } + | LOOKAHEAD(2) source = IntervalUnorderedNoOverlaps() { return source; } + | LOOKAHEAD(2) source = IntervalOr() { return source; } + | LOOKAHEAD(2) source = IntervalWildcard() { return source; } + | LOOKAHEAD(2) source = IntervalAfter() { return source; } + | LOOKAHEAD(2) source = IntervalBefore() { return source; } + | LOOKAHEAD(2) source = IntervalPhrase() { return source; } + | LOOKAHEAD(2) source = IntervalContaining() { return source; } + | LOOKAHEAD(2) source = IntervalNotContaining() { return source; } + | LOOKAHEAD(2) source = IntervalContainedBy() { return source; } + | LOOKAHEAD(2) source = IntervalNotContainedBy() { return source; } + | LOOKAHEAD(2) source = IntervalWithin() { return source; } + | LOOKAHEAD(2) source = IntervalNotWithin() { return source; } + | LOOKAHEAD(2) source = IntervalOverlapping() { return source; } + | LOOKAHEAD(2) source = IntervalNonOverlapping() { return source; } + | LOOKAHEAD(2) source = IntervalExtend() { return source; } + | LOOKAHEAD(2) source = IntervalFuzzyTerm() { return source; } + | LOOKAHEAD(2) source = IntervalText() { return source; } +} + +private IntervalFunction IntervalAtLeast() : { + IntervalFunction source; + ArrayList sources = new ArrayList(); + Token minShouldMatch; +} +{ + + minShouldMatch = (source = IntervalFun() { sources.add(source); })+ + { + return new AtLeast(parseInt(minShouldMatch), sources); + } +} + +private IntervalFunction IntervalMaxWidth() : { + IntervalFunction source; + Token maxWidth; +} +{ + + maxWidth = source = IntervalFun() + { + return new MaxWidth(parseInt(maxWidth), source); + } +} + +private IntervalFunction IntervalMaxGaps() : { + IntervalFunction source; + Token maxGaps; +} +{ + + maxGaps = source = IntervalFun() + { + return new MaxGaps(parseInt(maxGaps), source); + } +} + +private IntervalFunction IntervalUnordered() : { + IntervalFunction source; + ArrayList sources = new ArrayList(); +} +{ + + (source = IntervalFun() { sources.add(source); })+ + { + return new Unordered(sources); + } +} + +private IntervalFunction IntervalUnorderedNoOverlaps() : { + IntervalFunction a, b; +} +{ + + a = IntervalFun() b = IntervalFun() + { + return new UnorderedNoOverlaps(a, b); + } +} + +private IntervalFunction IntervalOrdered() : { + IntervalFunction source; + ArrayList sources = new ArrayList(); +} +{ + + (source = IntervalFun() { sources.add(source); })+ + { + return new Ordered(sources); + } +} + +private IntervalFunction IntervalOr() : { + IntervalFunction source; + ArrayList sources = new ArrayList(); +} +{ + + (source = IntervalFun() { sources.add(source); })+ + { + return new Or(sources); + } +} + +private IntervalFunction IntervalPhrase() : { + IntervalFunction source; + ArrayList sources = new ArrayList(); +} +{ + + (source = IntervalFun() { sources.add(source); })+ + { + return new Phrase(sources); + } +} + +private IntervalFunction IntervalBefore() : { + IntervalFunction source; + IntervalFunction reference; +} +{ + source = IntervalFun() reference = IntervalFun() + { + return new Before(source, reference); + } +} + +private IntervalFunction IntervalAfter() : { + IntervalFunction source; + IntervalFunction reference; +} +{ + source = IntervalFun() reference = IntervalFun() + { + return new After(source, reference); + } +} + +private IntervalFunction IntervalContaining() : { + IntervalFunction big; + IntervalFunction small; +} +{ + big = IntervalFun() small = IntervalFun() + { + return new Containing(big, small); + } +} + +private IntervalFunction IntervalNotContaining() : { + IntervalFunction minuend; + IntervalFunction subtrahend; +} +{ + minuend = IntervalFun() subtrahend = IntervalFun() + { + return new NotContaining(minuend, subtrahend); + } +} + +private IntervalFunction IntervalContainedBy() : { + IntervalFunction big; + IntervalFunction small; +} +{ + small = IntervalFun() big = IntervalFun() + { + return new ContainedBy(small, big); + } +} + +private IntervalFunction IntervalNotContainedBy() : { + IntervalFunction big; + IntervalFunction small; +} +{ + small = IntervalFun() big = IntervalFun() + { + return new NotContainedBy(small, big); + } +} + +private IntervalFunction IntervalWithin() : { + IntervalFunction source, reference; + Token positions; +} +{ + + + source = IntervalFun() + positions = + reference = IntervalFun() + + { + return new Within(source, parseInt(positions), reference); + } +} + +private IntervalFunction IntervalExtend() : { + IntervalFunction source; + Token before, after; +} +{ + + + source = IntervalFun() + before = + after = + + { + return new Extend(source, parseInt(before), parseInt(after)); + } +} + +private IntervalFunction IntervalNotWithin() : { + IntervalFunction minuend, subtrahend; + Token positions; +} +{ + + + minuend = IntervalFun() + positions = + subtrahend = IntervalFun() + + { + return new NotWithin(minuend, parseInt(positions), subtrahend); + } +} + +private IntervalFunction IntervalOverlapping() : { + IntervalFunction source, reference; +} +{ + source = IntervalFun() reference = IntervalFun() + { + return new Overlapping(source, reference); + } +} + +private IntervalFunction IntervalNonOverlapping() : { + IntervalFunction minuend, subtrahend; +} +{ + minuend = IntervalFun() subtrahend = IntervalFun() + { + return new NonOverlapping(minuend, subtrahend); + } +} + +private IntervalFunction IntervalWildcard() : { + String wildcard; + Token maxExpansions = null; +} +{ + + + ( + ( | ) { wildcard = token.image; } + | { wildcard = token.image.substring(1, token.image.length() - 1); } + ) + (maxExpansions = )? + + { + return new Wildcard(wildcard, maxExpansions == null ? 0 : parseInt(maxExpansions)); + } +} + +private IntervalFunction IntervalFuzzyTerm() : { + String term; + Token maxEdits = null; + Token maxExpansions = null; +} +{ + + + ( + ( | ) { term = token.image; } + | { term = token.image.substring(1, token.image.length() - 1); } + ) + (LOOKAHEAD(2) maxEdits = )? + (LOOKAHEAD(2) maxExpansions = )? + + { + return new FuzzyTerm(term, + maxEdits == null ? null : parseInt(maxEdits), + maxExpansions == null ? null : parseInt(maxExpansions)); + } +} + +private IntervalFunction IntervalText() : { +} +{ + () { return new AnalyzedText(token.image.substring(1, token.image.length() - 1)); } + | ( | ) { return new AnalyzedText(token.image); } +} + +/** + * Score boost modifier. + * + *
{@code
+ * Boost ::= '^' 
+ * }
+ */ +private QueryNode Boost(QueryNode node) : { + Token boost; +} +{ + boost = + { + return node == null ? node : new BoostQueryNode(node, parseFloat(boost)); + } +} + +/** + * Fuzzy term modifier. + * + *
{@code
+ * Fuzzy ::= '~' ?
+ * }
+ */ +private QueryNode FuzzyOp(CharSequence field, Token term, QueryNode node) : { + Token similarity = null; +} +{ + (LOOKAHEAD(2) similarity = )? + { + float fms = org.apache.lucene.search.FuzzyQuery.defaultMaxEdits; + if (similarity != null) { + fms = parseFloat(similarity); + if (fms < 0.0f) { + throw new ParseException(new MessageImpl(QueryParserMessages.INVALID_SYNTAX_FUZZY_LIMITS)); + } else if (fms >= 1.0f && fms != (int) fms) { + throw new ParseException(new MessageImpl(QueryParserMessages.INVALID_SYNTAX_FUZZY_EDITS)); + } + } + return new FuzzyQueryNode(field, discardEscapeChar(term.image), fms, term.beginColumn, term.endColumn); + } +} + +/** + * A field range expression selects all field values larger/ smaller (or equal) than a given one. + *
{@code
+ * FieldRangeExpr ::= FieldName ('<' | '>' | '<=' | '>=') ( |  | )
+ * }
+ */ +private TermRangeQueryNode FieldRangeExpr(CharSequence field) : { + Token operator, term; + FieldQueryNode qLower, qUpper; + boolean lowerInclusive, upperInclusive; +} +{ + field = FieldName() + ( | | | ) { operator = token; } + ( | | ) { term = token; } + { + if (term.kind == QUOTED) { + term.image = term.image.substring(1, term.image.length() - 1); + } + switch (operator.kind) { + case OP_LESSTHAN: + lowerInclusive = true; + upperInclusive = false; + qLower = new FieldQueryNode(field, "*", term.beginColumn, term.endColumn); + qUpper = new FieldQueryNode(field, discardEscapeChar(term.image), term.beginColumn, term.endColumn); + break; + case OP_LESSTHANEQ: + lowerInclusive = true; + upperInclusive = true; + qLower = new FieldQueryNode(field, "*", term.beginColumn, term.endColumn); + qUpper = new FieldQueryNode(field, discardEscapeChar(term.image), term.beginColumn, term.endColumn); + break; + case OP_MORETHAN: + lowerInclusive = false; + upperInclusive = true; + qLower = new FieldQueryNode(field, discardEscapeChar(term.image), term.beginColumn, term.endColumn); + qUpper = new FieldQueryNode(field, "*", term.beginColumn, term.endColumn); + break; + case OP_MORETHANEQ: + lowerInclusive = true; + upperInclusive = true; + qLower = new FieldQueryNode(field, discardEscapeChar(term.image), term.beginColumn, term.endColumn); + qUpper = new FieldQueryNode(field, "*", term.beginColumn, term.endColumn); + break; + default: + throw new Error("Unhandled case, operator=" + operator); + } + return new TermRangeQueryNode(qLower, qUpper, lowerInclusive, upperInclusive); + } +} + +/** + * A term expression. + * + *
{@code
+ * Term ::= ( | ) ('~' )? ('^' )?
+ *        |  ('^' )?
+ *        | TermRangeExpr ('^' )?
+ *        | QuotedTerm ('^' )?
+ * }
+ */ +private QueryNode Term(CharSequence field) : { + QueryNode q; + Token term, fuzzySlop=null; +} +{ + ( + term = + { + String v = term.image.substring(1, term.image.length() - 1); + q = new RegexpQueryNode(field, v, 0, v.length()); + } + | (term = | term = ) + { q = new FieldQueryNode(field, discardEscapeChar(term.image), term.beginColumn, term.endColumn); } + ( q = FuzzyOp(field, term, q) )? + | q = TermRangeExpr(field) + | q = QuotedTerm(field) + ) + ( q = Boost(q) )? + { + return q; + } +} + + +/** + * A quoted term (phrase). + * + *
{@code
+ * QuotedTerm ::=  ('~' )?
+ * }
+ */ +private QueryNode QuotedTerm(CharSequence field) : { + QueryNode q; + Token term, slop; +} +{ + term = + { + String image = term.image.substring(1, term.image.length() - 1); + q = new QuotedFieldQueryNode(field, discardEscapeChar(image), term.beginColumn + 1, term.endColumn - 1); + } + ( slop = { q = new SlopQueryNode(q, parseInt(slop)); } )? + { + return q; + } +} + +/** + * A value range expression. + * + *
{@code
+ * TermRangeExpr ::= ('[' | '{')  'TO'  (']' | '}')
+ * }
+ */ +private TermRangeQueryNode TermRangeExpr(CharSequence field) : { + Token left, right; + boolean leftInclusive = false; + boolean rightInclusive = false; +} +{ + // RANGE_TO can be consumed as range start/end because this needs to be accepted as a valid range: + // [TO TO TO] + ( + ( { leftInclusive = true; } | ) + ( | | ) { left = token; } + + ( | | ) { right = token; } + ( { rightInclusive = true; } | ) + ) + + { + if (left.kind == RANGE_QUOTED) { + left.image = left.image.substring(1, left.image.length() - 1); + } + if (right.kind == RANGE_QUOTED) { + right.image = right.image.substring(1, right.image.length() - 1); + } + + FieldQueryNode qLower = new FieldQueryNode(field, + discardEscapeChar(left.image), left.beginColumn, left.endColumn); + FieldQueryNode qUpper = new FieldQueryNode(field, + discardEscapeChar(right.image), right.beginColumn, right.endColumn); + + return new TermRangeQueryNode(qLower, qUpper, leftInclusive, rightInclusive); + } +} -- cgit v1.2.1