| /* |
| * 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.analytics.statistics; |
| |
| import java.text.ParseException; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| |
| import org.apache.lucene.queries.function.ValueSource; |
| import org.apache.lucene.queries.function.valuesource.BytesRefFieldSource; |
| import org.apache.lucene.queries.function.valuesource.DoubleFieldSource; |
| import org.apache.lucene.queries.function.valuesource.FloatFieldSource; |
| import org.apache.lucene.queries.function.valuesource.IntFieldSource; |
| import org.apache.lucene.queries.function.valuesource.LongFieldSource; |
| import org.apache.lucene.search.FieldCache; |
| import org.apache.solr.analytics.expression.ExpressionFactory; |
| import org.apache.solr.analytics.request.ExpressionRequest; |
| import org.apache.solr.analytics.util.AnalyticsParams; |
| import org.apache.solr.analytics.util.AnalyticsParsers; |
| import org.apache.solr.analytics.util.valuesource.AbsoluteValueDoubleFunction; |
| import org.apache.solr.analytics.util.valuesource.AddDoubleFunction; |
| import org.apache.solr.analytics.util.valuesource.ConcatStringFunction; |
| import org.apache.solr.analytics.util.valuesource.ConstDateSource; |
| import org.apache.solr.analytics.util.valuesource.ConstDoubleSource; |
| import org.apache.solr.analytics.util.valuesource.ConstStringSource; |
| import org.apache.solr.analytics.util.valuesource.DateFieldSource; |
| import org.apache.solr.analytics.util.valuesource.DateMathFunction; |
| import org.apache.solr.analytics.util.valuesource.DivDoubleFunction; |
| import org.apache.solr.analytics.util.valuesource.DualDoubleFunction; |
| import org.apache.solr.analytics.util.valuesource.FilterFieldSource; |
| import org.apache.solr.analytics.util.valuesource.LogDoubleFunction; |
| import org.apache.solr.analytics.util.valuesource.MultiDateFunction; |
| import org.apache.solr.analytics.util.valuesource.MultiDoubleFunction; |
| import org.apache.solr.analytics.util.valuesource.MultiplyDoubleFunction; |
| import org.apache.solr.analytics.util.valuesource.NegateDoubleFunction; |
| import org.apache.solr.analytics.util.valuesource.PowDoubleFunction; |
| import org.apache.solr.analytics.util.valuesource.ReverseStringFunction; |
| import org.apache.solr.analytics.util.valuesource.SingleDoubleFunction; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.SolrException.ErrorCode; |
| import org.apache.solr.schema.FieldType; |
| import org.apache.solr.schema.IndexSchema; |
| import org.apache.solr.schema.SchemaField; |
| import org.apache.solr.schema.StrField; |
| import org.apache.solr.schema.TrieDateField; |
| import org.apache.solr.schema.TrieDoubleField; |
| import org.apache.solr.schema.TrieFloatField; |
| import org.apache.solr.schema.TrieIntField; |
| import org.apache.solr.schema.TrieLongField; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import com.google.common.base.Supplier; |
| |
| public class StatsCollectorSupplierFactory { |
| private static final Logger log = LoggerFactory.getLogger(StatsCollectorSupplierFactory.class); |
| |
| // FunctionTypes |
| final static int NUMBER_TYPE = 0; |
| final static int DATE_TYPE = 1; |
| final static int STRING_TYPE = 2; |
| final static int FIELD_TYPE = 3; |
| final static int FILTER_TYPE = 4; |
| |
| /** |
| * Builds a Supplier that will generate identical arrays of new StatsCollectors. |
| * |
| * @param schema The Schema being used. |
| * @param exRequests The expression requests to generate a StatsCollector[] from. |
| * @return A Supplier that will return an array of new StatsCollector. |
| */ |
| @SuppressWarnings("unchecked") |
| public static Supplier<StatsCollector[]> create(IndexSchema schema, List<ExpressionRequest> exRequests ) { |
| final Map<String, Set<String>> collectorStats = new TreeMap<>(); |
| final Map<String, Set<Integer>> collectorPercs = new TreeMap<>(); |
| final Map<String, ValueSource> collectorSources = new TreeMap<>(); |
| |
| // Iterate through all expression request to make a list of ValueSource strings |
| // and statistics that need to be calculated on those ValueSources. |
| for (ExpressionRequest expRequest : exRequests) { |
| String statExpression = expRequest.getExpressionString(); |
| Set<String> statistics = getStatistics(statExpression); |
| if (statistics == null) { |
| continue; |
| } |
| for (String statExp : statistics) { |
| String stat; |
| String operands; |
| try { |
| stat = statExp.substring(0, statExp.indexOf('(')).trim(); |
| operands = statExp.substring(statExp.indexOf('(')+1, statExp.lastIndexOf(')')).trim(); |
| } catch (Exception e) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"Unable to parse statistic: ["+statExpression+"]",e); |
| } |
| String[] arguments = ExpressionFactory.getArguments(operands); |
| String source = arguments[0]; |
| if (stat.equals(AnalyticsParams.STAT_PERCENTILE)) { |
| // The statistic is a percentile, extra parsing is required |
| if (arguments.length<2) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"Too few arguments given for "+stat+"() in ["+statExp+"]."); |
| } else if (arguments.length>2) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"Too many arguments given for "+stat+"() in ["+statExp+"]."); |
| } |
| source = arguments[1]; |
| Set<Integer> percs = collectorPercs.get(source); |
| if (percs == null) { |
| percs = new HashSet<>(); |
| collectorPercs.put(source, percs); |
| } |
| try { |
| int perc = Integer.parseInt(arguments[0]); |
| if (perc>0 && perc<100) { |
| percs.add(perc); |
| } else { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The percentile in ["+statExp+"] is not between 0 and 100, exculsive."); |
| } |
| } catch (NumberFormatException e) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"\""+arguments[0]+"\" cannot be converted into a percentile.",e); |
| } |
| } else if (arguments.length>1) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"Too many arguments given for "+stat+"() in ["+statExp+"]."); |
| } else if (arguments.length==0) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"No arguments given for "+stat+"() in ["+statExp+"]."); |
| } |
| // Only unique ValueSources will be made; therefore statistics must be accumulated for |
| // each ValueSource, even across different expression requests |
| Set<String> stats = collectorStats.get(source); |
| if (stats == null) { |
| stats = new HashSet<>(); |
| collectorStats.put(source, stats); |
| } |
| if(AnalyticsParams.STAT_PERCENTILE.equals(stat)) { |
| stats.add(stat + "_"+ arguments[0]); |
| } else { |
| stats.add(stat); |
| } |
| } |
| } |
| String[] keys = collectorStats.keySet().toArray(new String[0]); |
| for (String sourceStr : keys) { |
| // Build one ValueSource for each unique value source string |
| ValueSource source = buildSourceTree(schema, sourceStr); |
| if (source == null) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The statistic ["+sourceStr+"] could not be parsed."); |
| } |
| String builtString = source.toString(); |
| collectorSources.put(builtString,source); |
| // Replace the user given string with the correctly built string |
| if (!builtString.equals(sourceStr)) { |
| Set<String> stats = collectorStats.remove(sourceStr); |
| if (stats!=null) { |
| collectorStats.put(builtString, stats); |
| } |
| Set<Integer> percs = collectorPercs.remove(sourceStr); |
| if (percs!=null) { |
| collectorPercs.put(builtString, percs); |
| } |
| for (ExpressionRequest er : exRequests) { |
| er.setExpressionString(er.getExpressionString().replace(sourceStr, builtString)); |
| } |
| } |
| } |
| if (collectorSources.size()==0) { |
| return new Supplier<StatsCollector[]>() { |
| @Override |
| public StatsCollector[] get() { |
| return new StatsCollector[0]; |
| } |
| }; |
| } |
| |
| log.info("Stats objects: "+collectorStats.size()+" sr="+collectorSources.size()+" pr="+collectorPercs.size() ); |
| |
| // All information is stored in final arrays so that nothing |
| // has to be computed when the Supplier's get() method is called. |
| final Set<String>[] statsArr = collectorStats.values().toArray(new Set[0]); |
| final ValueSource[] sourceArr = collectorSources.values().toArray(new ValueSource[0]); |
| final boolean[] uniqueBools = new boolean[statsArr.length]; |
| final boolean[] medianBools = new boolean[statsArr.length]; |
| final boolean[] numericBools = new boolean[statsArr.length]; |
| final boolean[] dateBools = new boolean[statsArr.length]; |
| final double[][] percsArr = new double[statsArr.length][]; |
| final String[][] percsNames = new String[statsArr.length][]; |
| for (int count = 0; count < sourceArr.length; count++) { |
| uniqueBools[count] = statsArr[count].contains(AnalyticsParams.STAT_UNIQUE); |
| medianBools[count] = statsArr[count].contains(AnalyticsParams.STAT_MEDIAN); |
| numericBools[count] = statsArr[count].contains(AnalyticsParams.STAT_SUM)||statsArr[count].contains(AnalyticsParams.STAT_SUM_OF_SQUARES)||statsArr[count].contains(AnalyticsParams.STAT_MEAN)||statsArr[count].contains(AnalyticsParams.STAT_STANDARD_DEVIATION); |
| dateBools[count] = (sourceArr[count] instanceof DateFieldSource) | (sourceArr[count] instanceof MultiDateFunction) | (sourceArr[count] instanceof ConstDateSource); |
| Set<Integer> ps = collectorPercs.get(sourceArr[count].toString()); |
| if (ps!=null) { |
| percsArr[count] = new double[ps.size()]; |
| percsNames[count] = new String[ps.size()]; |
| int percCount = 0; |
| for (int p : ps) { |
| percsArr[count][percCount] = p/100.0; |
| percsNames[count][percCount++] = AnalyticsParams.STAT_PERCENTILE+"_"+p; |
| } |
| } |
| } |
| // Making the Supplier |
| return new Supplier<StatsCollector[]>() { |
| public StatsCollector[] get() { |
| StatsCollector[] collectors = new StatsCollector[statsArr.length]; |
| for (int count = 0; count < statsArr.length; count++) { |
| if(numericBools[count]){ |
| StatsCollector sc = new NumericStatsCollector(sourceArr[count], statsArr[count]); |
| if(uniqueBools[count]) sc = new UniqueStatsCollector(sc); |
| if(medianBools[count]) sc = new MedianStatsCollector(sc); |
| if(percsArr[count]!=null) sc = new PercentileStatsCollector(sc,percsArr[count],percsNames[count]); |
| collectors[count]=sc; |
| } else if (dateBools[count]) { |
| StatsCollector sc = new MinMaxStatsCollector(sourceArr[count], statsArr[count]); |
| if(uniqueBools[count]) sc = new UniqueStatsCollector(sc); |
| if(medianBools[count]) sc = new DateMedianStatsCollector(sc); |
| if(percsArr[count]!=null) sc = new PercentileStatsCollector(sc,percsArr[count],percsNames[count]); |
| collectors[count]=sc; |
| } else { |
| StatsCollector sc = new MinMaxStatsCollector(sourceArr[count], statsArr[count]); |
| if(uniqueBools[count]) sc = new UniqueStatsCollector(sc); |
| if(medianBools[count]) sc = new MedianStatsCollector(sc); |
| if(percsArr[count]!=null) sc = new PercentileStatsCollector(sc,percsArr[count],percsNames[count]); |
| collectors[count]=sc; |
| } |
| } |
| return collectors; |
| } |
| }; |
| } |
| |
| /** |
| * Finds the set of statistics that must be computed for the expression. |
| * @param expression The string representation of an expression |
| * @return The set of statistics (sum, mean, median, etc.) found in the expression |
| */ |
| public static Set<String> getStatistics(String expression) { |
| HashSet<String> set = new HashSet<>(); |
| int firstParen = expression.indexOf('('); |
| if (firstParen>0) { |
| String topOperation = expression.substring(0,firstParen).trim(); |
| if (AnalyticsParams.ALL_STAT_SET.contains(topOperation)) { |
| set.add(expression); |
| } else if (!(topOperation.equals(AnalyticsParams.CONSTANT_NUMBER)||topOperation.equals(AnalyticsParams.CONSTANT_DATE)||topOperation.equals(AnalyticsParams.CONSTANT_STRING))) { |
| String operands = expression.substring(firstParen+1, expression.lastIndexOf(')')).trim(); |
| String[] arguments = ExpressionFactory.getArguments(operands); |
| for (String argument : arguments) { |
| Set<String> more = getStatistics(argument); |
| if (more!=null) { |
| set.addAll(more); |
| } |
| } |
| } |
| } |
| if (set.size()==0) { |
| return null; |
| } |
| return set; |
| } |
| |
| /** |
| * Builds a Value Source from a given string |
| * |
| * @param schema The schema being used. |
| * @param expression The string to be turned into an expression. |
| * @return The completed ValueSource |
| */ |
| private static ValueSource buildSourceTree(IndexSchema schema, String expression) { |
| return buildSourceTree(schema,expression,FIELD_TYPE); |
| } |
| |
| /** |
| * Builds a Value Source from a given string and a given source type |
| * |
| * @param schema The schema being used. |
| * @param expression The string to be turned into an expression. |
| * @param sourceType The type of source that must be returned. |
| * @return The completed ValueSource |
| */ |
| private static ValueSource buildSourceTree(IndexSchema schema, String expression, int sourceType) { |
| int expressionType = getSourceType(expression); |
| if (sourceType != FIELD_TYPE && expressionType != FIELD_TYPE && |
| expressionType != FILTER_TYPE && expressionType != sourceType) { |
| return null; |
| } |
| switch (expressionType) { |
| case NUMBER_TYPE : return buildNumericSource(schema, expression); |
| case DATE_TYPE : return buildDateSource(schema, expression); |
| case STRING_TYPE : return buildStringSource(schema, expression); |
| case FIELD_TYPE : return buildFieldSource(schema, expression, sourceType); |
| case FILTER_TYPE : return buildFilterSource(schema, expression.substring(expression.indexOf('(')+1,expression.lastIndexOf(')')), sourceType); |
| default : throw new SolrException(ErrorCode.BAD_REQUEST,expression+" is not a valid operation."); |
| } |
| } |
| |
| /** |
| * Determines what type of value source the expression represents. |
| * |
| * @param expression The expression representing the desired ValueSource |
| * @return NUMBER_TYPE, DATE_TYPE, STRING_TYPE or -1 |
| */ |
| private static int getSourceType(String expression) { |
| int paren = expression.indexOf('('); |
| if (paren<0) { |
| return FIELD_TYPE; |
| } |
| String operation = expression.substring(0,paren).trim(); |
| |
| if (AnalyticsParams.NUMERIC_OPERATION_SET.contains(operation)) { |
| return NUMBER_TYPE; |
| } else if (AnalyticsParams.DATE_OPERATION_SET.contains(operation)) { |
| return DATE_TYPE; |
| } else if (AnalyticsParams.STRING_OPERATION_SET.contains(operation)) { |
| return STRING_TYPE; |
| } else if (operation.equals(AnalyticsParams.FILTER)) { |
| return FILTER_TYPE; |
| } |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The operation \""+operation+"\" in ["+expression+"] is not supported."); |
| } |
| |
| /** |
| * Builds a value source for a given field, making sure that the field fits a given source type. |
| * @param schema the schema |
| * @param expressionString The name of the field to build a Field Source from. |
| * @param sourceType FIELD_TYPE for any type of field, NUMBER_TYPE for numeric fields, |
| * DATE_TYPE for date fields and STRING_TYPE for string fields. |
| * @return a value source |
| */ |
| private static ValueSource buildFieldSource(IndexSchema schema, String expressionString, int sourceType) { |
| SchemaField sf; |
| try { |
| sf = schema.getField(expressionString); |
| } catch (SolrException e) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The field "+expressionString+" does not exist.",e); |
| } |
| FieldType type = sf.getType(); |
| if ( type instanceof TrieIntField) { |
| if (sourceType!=NUMBER_TYPE&&sourceType!=FIELD_TYPE) { |
| return null; |
| } |
| return new IntFieldSource(expressionString, FieldCache.NUMERIC_UTILS_INT_PARSER) { |
| public String description() { |
| return field; |
| } |
| }; |
| } else if (type instanceof TrieLongField) { |
| if (sourceType!=NUMBER_TYPE&&sourceType!=FIELD_TYPE) { |
| return null; |
| } |
| return new LongFieldSource(expressionString, FieldCache.NUMERIC_UTILS_LONG_PARSER) { |
| public String description() { |
| return field; |
| } |
| }; |
| } else if (type instanceof TrieFloatField) { |
| if (sourceType!=NUMBER_TYPE&&sourceType!=FIELD_TYPE) { |
| return null; |
| } |
| return new FloatFieldSource(expressionString, FieldCache.NUMERIC_UTILS_FLOAT_PARSER) { |
| public String description() { |
| return field; |
| } |
| }; |
| } else if (type instanceof TrieDoubleField) { |
| if (sourceType!=NUMBER_TYPE&&sourceType!=FIELD_TYPE) { |
| return null; |
| } |
| return new DoubleFieldSource(expressionString, FieldCache.NUMERIC_UTILS_DOUBLE_PARSER) { |
| public String description() { |
| return field; |
| } |
| }; |
| } else if (type instanceof TrieDateField) { |
| if (sourceType!=DATE_TYPE&&sourceType!=FIELD_TYPE) { |
| return null; |
| } |
| return new DateFieldSource(expressionString, AnalyticsParsers.DEFAULT_DATE_PARSER) { |
| public String description() { |
| return field; |
| } |
| }; |
| } else if (type instanceof StrField) { |
| if (sourceType!=STRING_TYPE&&sourceType!=FIELD_TYPE) { |
| return null; |
| } |
| return new BytesRefFieldSource(expressionString) { |
| public String description() { |
| return field; |
| } |
| }; |
| } |
| throw new SolrException(ErrorCode.BAD_REQUEST, type.toString()+" is not a supported field type in Solr Analytics."); |
| } |
| |
| /** |
| * Builds a default is missing source that wraps a given source. A missing value is required for all |
| * non-field value sources. |
| * @param schema the schema |
| * @param expressionString The name of the field to build a Field Source from. |
| * @param sourceType FIELD_TYPE for any type of field, NUMBER_TYPE for numeric fields, |
| * DATE_TYPE for date fields and STRING_TYPE for string fields. |
| * @return a value source |
| */ |
| @SuppressWarnings("deprecation") |
| private static ValueSource buildFilterSource(IndexSchema schema, String expressionString, int sourceType) { |
| String[] arguments = ExpressionFactory.getArguments(expressionString); |
| if (arguments.length!=2) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"Invalid arguments were given for \""+AnalyticsParams.FILTER+"\"."); |
| } |
| ValueSource delegateSource = buildSourceTree(schema, arguments[0], sourceType); |
| if (delegateSource==null) { |
| return null; |
| } |
| Object defaultObject; |
| |
| ValueSource src = delegateSource; |
| if (delegateSource instanceof FilterFieldSource) { |
| src = ((FilterFieldSource)delegateSource).getRootSource(); |
| } |
| if ( src instanceof IntFieldSource) { |
| try { |
| defaultObject = new Integer(arguments[1]); |
| } catch (NumberFormatException e) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The filter value "+arguments[1]+" cannot be converted into an integer.",e); |
| } |
| } else if ( src instanceof DateFieldSource || src instanceof MultiDateFunction) { |
| try { |
| defaultObject = TrieDateField.parseDate(arguments[1]); |
| } catch (ParseException e) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The filter value "+arguments[1]+" cannot be converted into a date.",e); |
| } |
| } else if ( src instanceof LongFieldSource ) { |
| try { |
| defaultObject = new Long(arguments[1]); |
| } catch (NumberFormatException e) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The filter value "+arguments[1]+" cannot be converted into a long.",e); |
| } |
| } else if ( src instanceof FloatFieldSource ) { |
| try { |
| defaultObject = new Float(arguments[1]); |
| } catch (NumberFormatException e) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The filter value "+arguments[1]+" cannot be converted into a float.",e); |
| } |
| } else if ( src instanceof DoubleFieldSource || src instanceof SingleDoubleFunction || |
| src instanceof DualDoubleFunction|| src instanceof MultiDoubleFunction) { |
| try { |
| defaultObject = new Double(arguments[1]); |
| } catch (NumberFormatException e) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The filter value "+arguments[1]+" cannot be converted into a double.",e); |
| } |
| } else { |
| defaultObject = arguments[1]; |
| } |
| return new FilterFieldSource(delegateSource,defaultObject); |
| } |
| |
| /** |
| * Recursively parses and breaks down the expression string to build a numeric ValueSource. |
| * |
| * @param schema The schema to pull fields from. |
| * @param expressionString The expression string to build a ValueSource from. |
| * @return The value source represented by the given expressionString |
| */ |
| private static ValueSource buildNumericSource(IndexSchema schema, String expressionString) { |
| int paren = expressionString.indexOf('('); |
| String[] arguments; |
| String operands; |
| if (paren<0) { |
| return buildFieldSource(schema,expressionString,NUMBER_TYPE); |
| } else { |
| try { |
| operands = expressionString.substring(paren+1, expressionString.lastIndexOf(')')).trim(); |
| } catch (Exception e) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"Missing closing parenthesis in ["+expressionString+"]"); |
| } |
| arguments = ExpressionFactory.getArguments(operands); |
| } |
| String operation = expressionString.substring(0, paren).trim(); |
| if (operation.equals(AnalyticsParams.CONSTANT_NUMBER)) { |
| if (arguments.length!=1) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The constant number declaration ["+expressionString+"] does not have exactly 1 argument."); |
| } |
| return new ConstDoubleSource(Double.parseDouble(arguments[0])); |
| } else if (operation.equals(AnalyticsParams.NEGATE)) { |
| if (arguments.length!=1) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The negate operation ["+expressionString+"] does not have exactly 1 argument."); |
| } |
| ValueSource argSource = buildNumericSource(schema, arguments[0]); |
| if (argSource==null) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The operation \""+AnalyticsParams.NEGATE+"\" requires a numeric field or operation as argument. \""+arguments[0]+"\" is not a numeric field or operation."); |
| } |
| return new NegateDoubleFunction(argSource); |
| } else if (operation.equals(AnalyticsParams.ABSOLUTE_VALUE)) { |
| if (arguments.length!=1) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The absolute value operation ["+expressionString+"] does not have exactly 1 argument."); |
| } |
| ValueSource argSource = buildNumericSource(schema, arguments[0]); |
| if (argSource==null) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The operation \""+AnalyticsParams.NEGATE+"\" requires a numeric field or operation as argument. \""+arguments[0]+"\" is not a numeric field or operation."); |
| } |
| return new AbsoluteValueDoubleFunction(argSource); |
| } else if (operation.equals(AnalyticsParams.FILTER)) { |
| return buildFilterSource(schema, operands, NUMBER_TYPE); |
| } |
| List<ValueSource> subExpressions = new ArrayList<>(); |
| for (String argument : arguments) { |
| ValueSource argSource = buildNumericSource(schema, argument); |
| if (argSource == null) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The operation \""+operation+"\" requires numeric fields or operations as arguments. \""+argument+"\" is not a numeric field or operation."); |
| } |
| subExpressions.add(argSource); |
| } |
| if (operation.equals(AnalyticsParams.ADD)) { |
| return new AddDoubleFunction(subExpressions.toArray(new ValueSource[0])); |
| } else if (operation.equals(AnalyticsParams.MULTIPLY)) { |
| return new MultiplyDoubleFunction(subExpressions.toArray(new ValueSource[0])); |
| } else if (operation.equals(AnalyticsParams.DIVIDE)) { |
| if (subExpressions.size()!=2) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The divide operation ["+expressionString+"] does not have exactly 2 arguments."); |
| } |
| return new DivDoubleFunction(subExpressions.get(0),subExpressions.get(1)); |
| } else if (operation.equals(AnalyticsParams.POWER)) { |
| if (subExpressions.size()!=2) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The power operation ["+expressionString+"] does not have exactly 2 arguments."); |
| } |
| return new PowDoubleFunction(subExpressions.get(0),subExpressions.get(1)); |
| } else if (operation.equals(AnalyticsParams.LOG)) { |
| if (subExpressions.size()!=2) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The log operation ["+expressionString+"] does not have exactly 2 arguments."); |
| } |
| return new LogDoubleFunction(subExpressions.get(0), subExpressions.get(1)); |
| } |
| if (AnalyticsParams.DATE_OPERATION_SET.contains(operation)||AnalyticsParams.STRING_OPERATION_SET.contains(operation)) { |
| return null; |
| } |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The operation ["+expressionString+"] is not supported."); |
| } |
| |
| |
| /** |
| * Recursively parses and breaks down the expression string to build a date ValueSource. |
| * |
| * @param schema The schema to pull fields from. |
| * @param expressionString The expression string to build a ValueSource from. |
| * @return The value source represented by the given expressionString |
| */ |
| @SuppressWarnings("deprecation") |
| private static ValueSource buildDateSource(IndexSchema schema, String expressionString) { |
| int paren = expressionString.indexOf('('); |
| String[] arguments; |
| if (paren<0) { |
| return buildFieldSource(schema, expressionString, DATE_TYPE); |
| } else { |
| arguments = ExpressionFactory.getArguments(expressionString.substring(paren+1, expressionString.lastIndexOf(')')).trim()); |
| } |
| String operands = arguments[0]; |
| String operation = expressionString.substring(0, paren).trim(); |
| if (operation.equals(AnalyticsParams.CONSTANT_DATE)) { |
| if (arguments.length!=1) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The constant date declaration ["+expressionString+"] does not have exactly 1 argument."); |
| } |
| try { |
| return new ConstDateSource(TrieDateField.parseDate(operands)); |
| } catch (ParseException e) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The constant "+operands+" cannot be converted into a date.",e); |
| } |
| } else if (operation.equals(AnalyticsParams.FILTER)) { |
| return buildFilterSource(schema, operands, DATE_TYPE); |
| } |
| if (operation.equals(AnalyticsParams.DATE_MATH)) { |
| List<ValueSource> subExpressions = new ArrayList<>(); |
| boolean first = true; |
| for (String argument : arguments) { |
| ValueSource argSource; |
| if (first) { |
| first = false; |
| argSource = buildDateSource(schema, argument); |
| if (argSource == null) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"\""+AnalyticsParams.DATE_MATH+"\" requires the first argument be a date operation or field. ["+argument+"] is not a date operation or field."); |
| } |
| } else { |
| argSource = buildStringSource(schema, argument); |
| if (argSource == null) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"\""+AnalyticsParams.DATE_MATH+"\" requires that all arguments except the first be string operations. ["+argument+"] is not a string operation."); |
| } |
| } |
| subExpressions.add(argSource); |
| } |
| return new DateMathFunction(subExpressions.toArray(new ValueSource[0])); |
| } |
| if (AnalyticsParams.NUMERIC_OPERATION_SET.contains(operation)||AnalyticsParams.STRING_OPERATION_SET.contains(operation)) { |
| return null; |
| } |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The operation ["+expressionString+"] is not supported."); |
| } |
| |
| |
| /** |
| * Recursively parses and breaks down the expression string to build a string ValueSource. |
| * |
| * @param schema The schema to pull fields from. |
| * @param expressionString The expression string to build a ValueSource from. |
| * @return The value source represented by the given expressionString |
| */ |
| private static ValueSource buildStringSource(IndexSchema schema, String expressionString) { |
| int paren = expressionString.indexOf('('); |
| String[] arguments; |
| if (paren<0) { |
| return buildFieldSource(schema, expressionString, FIELD_TYPE); |
| } else { |
| arguments = ExpressionFactory.getArguments(expressionString.substring(paren+1, expressionString.lastIndexOf(')')).trim()); |
| } |
| String operands = arguments[0]; |
| String operation = expressionString.substring(0, paren).trim(); |
| if (operation.equals(AnalyticsParams.CONSTANT_STRING)) { |
| operands = expressionString.substring(paren+1, expressionString.lastIndexOf(')')); |
| return new ConstStringSource(operands); |
| } else if (operation.equals(AnalyticsParams.FILTER)) { |
| return buildFilterSource(schema,operands,FIELD_TYPE); |
| } else if (operation.equals(AnalyticsParams.REVERSE)) { |
| if (arguments.length!=1) { |
| throw new SolrException(ErrorCode.BAD_REQUEST,"\""+AnalyticsParams.REVERSE+"\" requires exactly one argument. The number of arguments in "+expressionString+" is not 1."); |
| } |
| return new ReverseStringFunction(buildStringSource(schema, operands)); |
| } |
| List<ValueSource> subExpressions = new ArrayList<>(); |
| for (String argument : arguments) { |
| subExpressions.add(buildSourceTree(schema, argument)); |
| } |
| if (operation.equals(AnalyticsParams.CONCATENATE)) { |
| return new ConcatStringFunction(subExpressions.toArray(new ValueSource[0])); |
| } |
| if (AnalyticsParams.NUMERIC_OPERATION_SET.contains(operation)) { |
| return buildNumericSource(schema, expressionString); |
| } else if (AnalyticsParams.DATE_OPERATION_SET.contains(operation)) { |
| return buildDateSource(schema, expressionString); |
| } |
| throw new SolrException(ErrorCode.BAD_REQUEST,"The operation ["+expressionString+"] is not supported."); |
| } |
| } |