// This file is part of OpenTSDB. // Copyright (C) 2015 The OpenTSDB Authors. // // This program 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 program 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 program. If not, // see <http://www.gnu.org/licenses/>. package net.opentsdb.query.expression; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import net.opentsdb.core.DataPoints; import net.opentsdb.core.IllegalDataException; import net.opentsdb.core.TSQuery; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; /** * A node in a tree of nested expressions. The tree may link to other nodes as * sub expressions. Evaluating the tree evaluates all sub expressions. * <p> * Before calling {@link evaluate} you MUST call a one or a combination of * {@link addSubExpression}, {@link addSubMetricQuery} and optionally * {@link addFunctionParameter} * <p> * TODO(cl) - Cleanup needed. Tracking the indices can likely be done better * and it would be good to have a ctor that sets the sub or metric query. * @since 2.3 */ public class ExpressionTree { /** Used for the toString() helpers */ private static final Joiner DOUBLE_COMMA_JOINER = Joiner.on(",").skipNulls(); /** An enumerator of the different query types */ enum Parameter { SUB_EXPRESSION, METRIC_QUERY } /** The root expression for the tree */ private final Expression expression; /** The original time series query */ private final TSQuery data_query; /** An optional list of sub expressions */ private List<ExpressionTree> sub_expressions; /** A list of parameters for the root expression */ private List<String> func_params; /** A mapping of result indices to sub metric queries */ private Map<Integer, String> sub_metric_queries; /** A mapping of query types to their result index */ private Map<Integer, Parameter> parameter_index = Maps.newHashMap(); /** * Creates a tree with a root and no children * @param expression_name The name of the expression to lookup in the factory * @param data_query The original query * @throws UnsupportedOperationException if the expression is not implemented */ public ExpressionTree(final String expression_name, final TSQuery data_query) { this(ExpressionFactory.getByName(expression_name), data_query); } /** * Creates a tree with a root and no children * @param expression The expression to use * @param data_query The original query */ public ExpressionTree(final Expression expression, final TSQuery data_query) { this.expression = expression; this.data_query = data_query; } public void addSubExpression(final ExpressionTree child, final int param_index) { if (child == null) { throw new IllegalArgumentException("Cannot add a null child tree"); } if (child == this) { throw new IllegalDataException("Recursive sub expression detected: " + this); } if (param_index < 0) { throw new IllegalArgumentException("Parameter index must be 0 or greater"); } if (sub_expressions == null) { sub_expressions = Lists.newArrayList(); } sub_expressions.add(child); parameter_index.put(param_index, Parameter.SUB_EXPRESSION); } /** * Sets the metric query key and index, setting the Parameter type to * METRIC_QUERY * @param metric_query The metric query id * @param sub_query_index The index of the metric query * @param param_index The index of the parameter (??) */ public void addSubMetricQuery(final String metric_query, final int sub_query_index, final int param_index) { if (metric_query == null || metric_query.isEmpty()) { throw new IllegalArgumentException("Metric query cannot be null or empty"); } if (sub_query_index < 0) { throw new IllegalArgumentException("Sub query index must be 0 or greater"); } if (param_index < 0) { throw new IllegalArgumentException("Parameter index must be 0 or greater"); } if (sub_metric_queries == null) { sub_metric_queries = Maps.newHashMap(); } sub_metric_queries.put(sub_query_index, metric_query); parameter_index.put(param_index, Parameter.METRIC_QUERY); } /** * Adds parameters for the root expression only. * @param param The parameter to add, cannot be null or empty * @throws IllegalArgumentException if the parameter is null or empty */ public void addFunctionParameter(final String param) { if (param == null || param.isEmpty()) { throw new IllegalArgumentException("Parameter cannot be null or empty"); } if (func_params == null) { func_params = Lists.newArrayList(); } func_params.add(param); } /** * Processes the expression tree, including sub expressions, and returns the * results. * TODO(cl) - More tests around indices, etc. This can likely be cleaned up. * @param query_results The result set to pass to the expressions * @return The result set or an exception will bubble up if something wasn't * configured properly. */ public DataPoints[] evaluate(final List<DataPoints[]> query_results) { // TODO - size the array final List<DataPoints[]> materialized = Lists.newArrayList(); List<Integer> metric_query_keys = null; if (sub_metric_queries != null && sub_metric_queries.size() > 0) { metric_query_keys = Lists.newArrayList(sub_metric_queries.keySet()); Collections.sort(metric_query_keys); } int metric_pointer = 0; int sub_expression_pointer = 0; for (int i = 0; i < parameter_index.size(); i++) { final Parameter param = parameter_index.get(i); if (param == Parameter.METRIC_QUERY) { if (metric_query_keys == null) { throw new RuntimeException("Attempt to read metric " + "results when none exist"); } final int ix = metric_query_keys.get(metric_pointer++); materialized.add(query_results.get(ix)); } else if (param == Parameter.SUB_EXPRESSION) { final ExpressionTree st = sub_expressions.get(sub_expression_pointer++); materialized.add(st.evaluate(query_results)); } else { throw new IllegalDataException("Unknown parameter type: " + param + " in tree: " + this); } } return expression.evaluate(data_query, materialized, func_params); } @Override public String toString() { return writeStringField(); } /** * Helper to create the original expression (or at least a nested expression * without the parameters included) * @return A string representing the full expression. */ public String writeStringField() { final List<String> strs = Lists.newArrayList(); if (sub_expressions != null) { for (ExpressionTree sub : sub_expressions) { strs.add(sub.toString()); } } if (sub_metric_queries != null) { final String sub_metrics = clean(sub_metric_queries.values()); if (sub_metrics != null && sub_metrics.length() > 0) { strs.add(sub_metrics); } } final String inner_expression = DOUBLE_COMMA_JOINER.join(strs); return expression.writeStringField(func_params, inner_expression); } /** * Helper to clean out some characters * @param values The collection of strings to cleanup * @return An empty string if values was empty or a cleaned up string */ private String clean(final Collection<String> values) { if (values == null || values.size() == 0) { return ""; } final List<String> strs = Lists.newArrayList(); for (String v : values) { final String tmp = v.replaceAll("\\{.*\\}", ""); final int ix = tmp.lastIndexOf(':'); if (ix < 0) { strs.add(tmp); } else { strs.add(tmp.substring(ix+1)); } } return DOUBLE_COMMA_JOINER.join(strs); } @VisibleForTesting List<ExpressionTree> subExpressions() { return sub_expressions; } @VisibleForTesting List<String> funcParams() { return func_params; } @VisibleForTesting Map<Integer, String> subMetricQueries() { return sub_metric_queries; } @VisibleForTesting Map<Integer, Parameter> parameterIndex() { return parameter_index; } }