org.apache.solr.search.FunctionQParser.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.solr.search.FunctionQParser.java

Source

/*
 * 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.
 */
package org.apache.solr.search;

import java.util.ArrayList;
import java.util.List;

import org.apache.lucene.queries.function.FunctionQuery;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.valuesource.ConstValueSource;
import org.apache.lucene.queries.function.valuesource.DoubleConstValueSource;
import org.apache.lucene.queries.function.valuesource.LiteralValueSource;
import org.apache.lucene.queries.function.valuesource.QueryValueSource;
import org.apache.lucene.queries.function.valuesource.VectorValueSource;
import org.apache.lucene.search.Query;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.search.facet.AggValueSource;
import org.apache.solr.search.function.FieldNameValueSource;

public class FunctionQParser extends QParser {

    public static final int FLAG_CONSUME_DELIMITER = 0x01; // consume delimiter after parsing arg
    public static final int FLAG_IS_AGG = 0x02;
    public static final int FLAG_USE_FIELDNAME_SOURCE = 0x04; // When a field name is encountered, use the placeholder FieldNameValueSource instead of resolving to a real ValueSource
    public static final int FLAG_DEFAULT = FLAG_CONSUME_DELIMITER;

    /** @lucene.internal */
    public StrParser sp;
    boolean parseMultipleSources = true;
    boolean parseToEnd = true;

    public FunctionQParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) {
        super(qstr, localParams, params, req);
        setString(qstr);
    }

    @Override
    public void setString(String s) {
        super.setString(s);
        if (s != null) {
            sp = new StrParser(s);
        }
    }

    public void setParseMultipleSources(boolean parseMultipleSources) {
        this.parseMultipleSources = parseMultipleSources;
    }

    /** parse multiple comma separated value sources */
    public boolean getParseMultipleSources() {
        return parseMultipleSources;
    }

    public void setParseToEnd(boolean parseToEnd) {
        this.parseToEnd = parseToEnd;
    }

    /** throw exception if there is extra stuff at the end of the parsed valuesource(s). */
    public boolean getParseToEnd() {
        return parseMultipleSources;
    }

    @Override
    public Query parse() throws SyntaxError {
        ValueSource vs = null;
        List<ValueSource> lst = null;

        for (;;) {
            ValueSource valsource = parseValueSource(FLAG_DEFAULT & ~FLAG_CONSUME_DELIMITER);
            sp.eatws();
            if (!parseMultipleSources) {
                vs = valsource;
                break;
            } else {
                if (lst != null) {
                    lst.add(valsource);
                } else {
                    vs = valsource;
                }
            }

            // check if there is a "," separator
            if (sp.peek() != ',')
                break;

            consumeArgumentDelimiter();

            if (lst == null) {
                lst = new ArrayList<>(2);
                lst.add(valsource);
            }
        }

        if (parseToEnd && sp.pos < sp.end) {
            throw new SyntaxError("Unexpected text after function: " + sp.val.substring(sp.pos, sp.end));
        }

        if (lst != null) {
            vs = new VectorValueSource(lst);
        }

        return new FunctionQuery(vs);
    }

    /**
     * Are there more arguments in the argument list being parsed?
     * 
     * @return whether more args exist
     */
    public boolean hasMoreArguments() throws SyntaxError {
        int ch = sp.peek();
        /* determine whether the function is ending with a paren or end of str */
        return (!(ch == 0 || ch == ')'));
    }

    /*
     * TODO: Doc
     */
    public String parseId() throws SyntaxError {
        String value = parseArg();
        if (argWasQuoted()) {
            throw new SyntaxError("Expected identifier instead of quoted string:" + value);
        } else if (value == null) {
            throw new SyntaxError("Expected identifier instead of 'null' for function " + sp);
        }
        return value;
    }

    /**
     * Parse a float.
     * 
     * @return Float
     */
    public Float parseFloat() throws SyntaxError {
        String str = parseArg();
        if (argWasQuoted())
            throw new SyntaxError("Expected float instead of quoted string:" + str);
        try {
            return Float.parseFloat(str);
        } catch (NumberFormatException | NullPointerException e) {
            throw new SyntaxError("Expected float instead of '" + str + "' for function " + sp);
        }
    }

    /**
     * Parse a Double
     * @return double
     */
    public double parseDouble() throws SyntaxError {
        String str = parseArg();
        if (argWasQuoted())
            throw new SyntaxError("Expected double instead of quoted string:" + str);
        try {
            return Double.parseDouble(str);
        } catch (NumberFormatException | NullPointerException e) {
            throw new SyntaxError("Expected double instead of '" + str + "' for function " + sp);
        }
    }

    /**
     * Parse an integer
     * @return An int
     */
    public int parseInt() throws SyntaxError {
        String str = parseArg();
        if (argWasQuoted())
            throw new SyntaxError("Expected integer instead of quoted string:" + str);
        try {
            return Integer.parseInt(str);
        } catch (NumberFormatException | NullPointerException e) {
            throw new SyntaxError("Expected integer instead of '" + str + "' for function " + sp);
        }
    }

    private boolean argWasQuoted;

    public boolean argWasQuoted() {
        return argWasQuoted;
    }

    public String parseArg() throws SyntaxError {
        argWasQuoted = false;

        sp.eatws();
        char ch = sp.peek();
        String val = null;
        switch (ch) {
        case ')':
            return null;
        case '$':
            sp.pos++;
            String param = sp.getId();
            val = getParam(param);
            break;
        case '\'':
        case '"':
            val = sp.getQuotedString();
            argWasQuoted = true;
            break;
        default:
            // read unquoted literal ended by whitespace ',' or ')'
            // there is no escaping.
            int valStart = sp.pos;
            for (;;) {
                if (sp.pos >= sp.end) {
                    throw new SyntaxError(
                            "Missing end to unquoted value starting at " + valStart + " str='" + sp.val + "'");
                }
                char c = sp.val.charAt(sp.pos);
                if (c == ')' || c == ',' || Character.isWhitespace(c)) {
                    val = sp.val.substring(valStart, sp.pos);
                    break;
                }
                sp.pos++;
            }
        }

        sp.eatws();
        consumeArgumentDelimiter();
        return val;
    }

    /**
     * Parse a list of ValueSource.  Must be the final set of arguments
     * to a ValueSource.
     * 
     * @return List&lt;ValueSource&gt;
     */
    public List<ValueSource> parseValueSourceList() throws SyntaxError {
        List<ValueSource> sources = new ArrayList<>(3);
        while (hasMoreArguments()) {
            sources.add(parseValueSource(FLAG_DEFAULT | FLAG_CONSUME_DELIMITER));
        }
        return sources;
    }

    /**
     * Parse an individual ValueSource.
     */
    public ValueSource parseValueSource() throws SyntaxError {
        /* consume the delimiter afterward for an external call to parseValueSource */
        return parseValueSource(FLAG_DEFAULT | FLAG_CONSUME_DELIMITER);
    }

    /*
     * TODO: Doc
     */
    public Query parseNestedQuery() throws SyntaxError {
        Query nestedQuery;

        if (sp.opt("$")) {
            String param = sp.getId();
            String qstr = getParam(param);
            qstr = qstr == null ? "" : qstr;
            nestedQuery = subQuery(qstr, null).getQuery();

            // nestedQuery would be null when de-referenced query value is not specified
            // Ex: query($qq) in request with no qq param specified
            if (nestedQuery == null) {
                throw new SyntaxError("Missing param " + param + " while parsing function '" + sp.val + "'");
            }
        } else {
            int start = sp.pos;
            String v = sp.val;

            String qs = v;
            ModifiableSolrParams nestedLocalParams = new ModifiableSolrParams();
            int end = QueryParsing.parseLocalParams(qs, start, nestedLocalParams, getParams());

            QParser sub;

            if (end > start) {
                if (nestedLocalParams.get(QueryParsing.V) != null) {
                    // value specified directly in local params... so the end of the
                    // query should be the end of the local params.
                    sub = subQuery(qs.substring(start, end), null);
                } else {
                    // value here is *after* the local params... ask the parser.
                    sub = subQuery(qs, null);
                    // int subEnd = sub.findEnd(')');
                    // TODO.. implement functions to find the end of a nested query
                    throw new SyntaxError("Nested local params must have value in v parameter.  got '" + qs + "'");
                }
            } else {
                throw new SyntaxError(
                        "Nested function query must use $param or {!v=value} forms. got '" + qs + "'");
            }

            sp.pos += end - start; // advance past nested query
            nestedQuery = sub.getQuery();
            // handling null check on nestedQuery separately, so that proper error can be returned
            // one case this would be possible when v is specified but v's value is empty or has only spaces
            if (nestedQuery == null) {
                throw new SyntaxError("Nested function query returned null for '" + sp.val + "'");
            }
        }
        consumeArgumentDelimiter();

        return nestedQuery;
    }

    /**
     * Parse an individual value source.
     * 
     * @param doConsumeDelimiter whether to consume a delimiter following the ValueSource  
     */
    protected ValueSource parseValueSource(boolean doConsumeDelimiter) throws SyntaxError {
        return parseValueSource(doConsumeDelimiter ? (FLAG_DEFAULT | FLAG_CONSUME_DELIMITER)
                : (FLAG_DEFAULT & ~FLAG_CONSUME_DELIMITER));
    }

    protected ValueSource parseValueSource(int flags) throws SyntaxError {
        ValueSource valueSource;

        int ch = sp.peek();
        if (ch >= '0' && ch <= '9' || ch == '.' || ch == '+' || ch == '-') {
            Number num = sp.getNumber();
            if (num instanceof Long) {
                valueSource = new LongConstValueSource(num.longValue());
            } else if (num instanceof Double) {
                valueSource = new DoubleConstValueSource(num.doubleValue());
            } else {
                // shouldn't happen
                valueSource = new ConstValueSource(num.floatValue());
            }
        } else if (ch == '"' || ch == '\'') {
            valueSource = new LiteralValueSource(sp.getQuotedString());
        } else if (ch == '$') {
            sp.pos++;
            String param = sp.getId();
            String val = getParam(param);
            if (val == null) {
                throw new SyntaxError("Missing param " + param + " while parsing function '" + sp.val + "'");
            }

            QParser subParser = subQuery(val, "func");
            if (subParser instanceof FunctionQParser) {
                ((FunctionQParser) subParser).setParseMultipleSources(true);
            }
            Query subQuery = subParser.getQuery();
            if (subQuery instanceof FunctionQuery) {
                valueSource = ((FunctionQuery) subQuery).getValueSource();
            } else {
                valueSource = new QueryValueSource(subQuery, 0.0f);
            }

            /***
             // dereference *simple* argument (i.e., can't currently be a function)
             // In the future we could support full function dereferencing via a stack of ValueSource (or StringParser) objects
            ch = val.length()==0 ? '\0' : val.charAt(0);
                
            if (ch>='0' && ch<='9'  || ch=='.' || ch=='+' || ch=='-') {
              StrParser sp = new StrParser(val);
              Number num = sp.getNumber();
              if (num instanceof Long) {
                valueSource = new LongConstValueSource(num.longValue());
              } else if (num instanceof Double) {
                valueSource = new DoubleConstValueSource(num.doubleValue());
              } else {
                // shouldn't happen
                valueSource = new ConstValueSource(num.floatValue());
              }
            } else if (ch == '"' || ch == '\'') {
              StrParser sp = new StrParser(val);
              val = sp.getQuotedString();
              valueSource = new LiteralValueSource(val);
            } else {
              if (val.length()==0) {
                valueSource = new LiteralValueSource(val);
              } else {
                String id = val;
                SchemaField f = req.getSchema().getField(id);
                valueSource = f.getType().getValueSource(f, this);
              }
            }
             ***/

        } else {

            String id = sp.getId();
            if (sp.opt("(")) {
                // a function... look it up.
                ValueSourceParser argParser = req.getCore().getValueSourceParser(id);
                if (argParser == null) {
                    throw new SyntaxError("Unknown function " + id + " in FunctionQuery(" + sp + ")");
                }
                valueSource = argParser.parse(this);
                sp.expect(")");
            } else {
                if ("true".equals(id)) {
                    valueSource = new BoolConstValueSource(true);
                } else if ("false".equals(id)) {
                    valueSource = new BoolConstValueSource(false);
                } else {
                    if ((flags & FLAG_USE_FIELDNAME_SOURCE) != 0) {
                        // Don't try to create a ValueSource for the field, just use a placeholder.
                        valueSource = new FieldNameValueSource(id);
                    } else {
                        SchemaField f = req.getSchema().getField(id);
                        valueSource = f.getType().getValueSource(f, this);
                    }
                }
            }

        }

        if ((flags & FLAG_CONSUME_DELIMITER) != 0) {
            consumeArgumentDelimiter();
        }

        return valueSource;
    }

    /** @lucene.experimental */
    public AggValueSource parseAgg(int flags) throws SyntaxError {
        String id = sp.getId();
        AggValueSource vs = null;
        boolean hasParen = false;

        if ("agg".equals(id)) {
            hasParen = sp.opt("(");
            vs = parseAgg(flags | FLAG_IS_AGG);
        } else {
            // parse as an aggregation...
            if (!id.startsWith("agg_")) {
                id = "agg_" + id;
            }

            hasParen = sp.opt("(");

            ValueSourceParser argParser = req.getCore().getValueSourceParser(id);
            if (argParser == null) {
                throw new SyntaxError("Unknown aggregation " + id + " in (" + sp + ")");
            }

            ValueSource vv = argParser.parse(this);
            if (!(vv instanceof AggValueSource)) {
                if (argParser == null) { // why this??
                    throw new SyntaxError(
                            "Expected aggregation from " + id + " but got (" + vv + ") in (" + sp + ")");
                }
            }
            vs = (AggValueSource) vv;
        }

        if (hasParen) {
            sp.expect(")");
        }

        if ((flags & FLAG_CONSUME_DELIMITER) != 0) {
            consumeArgumentDelimiter();
        }

        return vs;
    }

    /**
     * Consume an argument delimiter (a comma) from the token stream.
     * Only consumes if more arguments should exist (no ending parens or end of string).
     * 
     * @return whether a delimiter was consumed
     */
    protected boolean consumeArgumentDelimiter() throws SyntaxError {
        /* if a list of args is ending, don't expect the comma */
        if (hasMoreArguments()) {
            sp.expect(",");
            return true;
        }

        return false;
    }

}