Package com.google.visualization.datasource.query

Source Code of com.google.visualization.datasource.query.Query

// Copyright 2009 Google Inc.
//
// Licensed 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 com.google.visualization.datasource.query;

import org.apache.commons.lang3.text.StrBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.util.List;
import java.util.Set;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.visualization.datasource.base.InvalidQueryException;
import com.google.visualization.datasource.base.MessagesEnum;
import com.ibm.icu.util.ULocale;

/**
* Holds the query data and clauses. This class is the result of parsing the query
* string that comes from the user.
* The clauses that can be included in a query are: select, filter, sort, group by, pivot, options,
* labels, format, limit, offset and skipping.
* For more details see the
* <a href="http://code.google.com/apis/visualization/documentation/querylanguage.html">
* query language reference
* </a>.
* A Query can be executed by the query engine
* (see: {@link com.google.visualization.datasource.DataSourceService#applyQuery}).
* It can be split in 2 Query objects based on the Capabilities value
* (see: {@link com.google.visualization.datasource.DataSourceService#splitQuery}).
* <p/>
* Note on errors: Since this class handles a user generated query, all errors
* (of type INVALID_QUERY) should provide a descriptive error message for both
* the user.
*
* @author Itai R.
* @author Yonatan B.Y.
*/
public class Query {
    private static final Log log = LogFactory.getLog(Query.class);

    /**
     * Checks the given list for duplicates. Throws an exception if a duplicate
     * is found, giving as a message a description containing information on which
     * item was found to be duplicate, and in which clause it occurred (the
     * clause name is given).
     *
     * @param selectionColumns The list to check for duplicates.
     * @param clauseName       The clause name to report in the exception message.
     * @param userLocale       The user locale.
     * @throws InvalidQueryException Thrown if a duplicate was found.
     */
    private static <T> void checkForDuplicates(List<T>
                                                       selectionColumns, String clauseName, ULocale userLocale) throws InvalidQueryException {
        for(int i = 0; i < selectionColumns.size(); i++) {
            T col = selectionColumns.get(i);
            for(int j = i + 1; j < selectionColumns.size(); j++) {
                if(col.equals(selectionColumns.get(j))) {
                    String[] args = {col.toString(), clauseName};
                    String messageToLogAndUser = MessagesEnum.COLUMN_ONLY_ONCE.getMessageWithArgs(userLocale,
                            args);
                    log.error(messageToLogAndUser);
                    throw new InvalidQueryException(messageToLogAndUser);
                }
            }
        }
    }

    /**
     * The required sort order.
     */
    private QuerySort sort = null;

    /**
     * The required selection.
     * If the selection is null, or is empty, the original
     * selection that was defined in the report is used.
     */
    protected QuerySelection selection = null;

    /**
     * The required filter.
     * If the filter is null, then the results are not filtered at all.
     */
    private QueryFilter filter = null;

    /**
     * The required grouping.
     */
    private QueryGroup group = null;

    /**
     * The required pivoting.
     */
    private QueryPivot pivot = null;

    /**
     * The number of rows to skip when selecting only a subset of the rows using
     * skipping clause, e.g., skipping 4.
     * If a skipping clause, "skip k",  is added to the query, the resulting table
     * will be consisted of the first row of every k rows of the table returned by
     * the earlier part of the query, when k corresponds to rowSkipping value.
     * For example, if rowSkipping = 10 the returned table will consist of rows 0, 10...
     * The default value is 0, meaning no skipping should be performed.
     */
    private int rowSkipping = 0;

    /**
     * Max number of rows to return to caller.
     * If the caller specified this parameter, and the data table returned from
     * the data source contains more than this number of rows, then the result is
     * truncated, and the query result contains a warning with this reason.
     * If this value is set to -1, which can only be done internally, this is ignored.
     * (0 is a legal value, denoting no rows should be retrieved).
     */
    private int rowLimit = -1;

    /**
     * The number of rows that should be removed from the beginning of the
     * data table.
     * Together with the row limit parameter, this enables pagination.
     * The default value is 0, (skip 0 rows) - which means start from the
     * first row.
     */
    private int rowOffset = 0;

    /**
     * Additional options for the query.
     */
    private QueryOptions options = null;

    /**
     * Column labels as specified in the query.
     */
    private QueryLabels labels = null;

    /**
     * Column formatting patterns as specified in the query.
     */
    private QueryFormat userFormatOptions = null;

    /**
     * The user locale, Used to create localized messages.
     */
    private ULocale localeForUserMessages = null;

    /**
     * Constructs a new, empty, query.
     */
    public Query() {
    }

    /**
     * Set the required sort of the query result.
     *
     * @param sort The required sort of the query result.
     */
    public void setSort(QuerySort sort) {
        this.sort = sort;
    }

    /**
     * Returns the required sort of the query result.
     *
     * @return The required sort of the query result.
     */
    public QuerySort getSort() {
        return sort;
    }

    /**
     * Returns true if the query has a non empty sort-by section.
     *
     * @return True if the query has a non empty sort-by section.
     */
    public boolean hasSort() {
        return (sort != null) && (!sort.isEmpty());
    }

    /**
     * Set the required selection of the query result.
     *
     * @param selection The required selection of the query result.
     */
    public void setSelection(QuerySelection selection) {
        this.selection = selection;
    }

    /**
     * Returns the required selection of the query result.
     *
     * @return The required selection of the query result.
     */
    public QuerySelection getSelection() {
        return selection;
    }

    /**
     * Returns true if there is a non-trivial selection, i.e. if not all columns
     * should be selected.
     *
     * @return True if there is a non-trivial selection.
     */
    public boolean hasSelection() {
        return (selection != null) && (!selection.isEmpty());
    }

    /**
     * Set the required filter of this query.
     *
     * @param filter The required filter of this query.
     */
    public void setFilter(QueryFilter filter) {
        this.filter = filter;
    }

    /**
     * Returns the required filter of this query.
     *
     * @return The required filter of this query.
     */
    public QueryFilter getFilter() {
        return filter;
    }

    /**
     * Returns whether or not this query has a filter defined.
     *
     * @return true if this query has a filter defined.
     */
    public boolean hasFilter() {
        return (filter != null);
    }

    /**
     * Sets the required group of the query result.
     *
     * @param group The required group of the query result.
     */
    public void setGroup(QueryGroup group) {
        this.group = group;
    }

    /**
     * Sets the required pivot of the query result.
     *
     * @param pivot The required pivot of the query result.
     */
    public void setPivot(QueryPivot pivot) {
        this.pivot = pivot;
    }

    /**
     * Returns the required group of the query result.
     *
     * @return The required group of the query result.
     */
    public QueryGroup getGroup() {
        return group;
    }

    /**
     * Returns true if the query has a group-by section.
     *
     * @return True if the query has a group-by section.
     */
    public boolean hasGroup() {
        return group != null && !group.getColumnIds().isEmpty();
    }

    /**
     * Returns the required pivot of the query result.
     *
     * @return The required pivot of the query result.
     */
    public QueryPivot getPivot() {
        return pivot;
    }

    /**
     * Returns true if the query has a pivot-by section.
     *
     * @return True if the query has a pivot-by section.
     */
    public boolean hasPivot() {
        return pivot != null && !pivot.getColumnIds().isEmpty();
    }

    /**
     * Returns the number of rows to skip when selecting only a subset of
     * the rows using skipping clause, e.g., skipping 4.
     * The default value is 0, meaning no skipping should be performed.
     *
     * @return The number of rows to skip between each row selection.
     */
    public int getRowSkipping() {
        return rowSkipping;
    }

    /**
     * Sets the number of rows to skip when selecting only a subset of
     * the rows using skipping clause, e.g., skipping 4.
     * The default value is 0, meaning no skipping should be performed.
     * <p/>
     * If there is is an attempt to set the skipping value to non positive value,
     * then an InvalidQueryException is thrown.
     *
     * @param rowSkipping The number of rows to skip between each row selection.
     *                    0 value means no skipping.
     * @throws InvalidQueryException Thrown if an invalid value is specified.
     */
    public void setRowSkipping(int rowSkipping) throws InvalidQueryException {
        if(rowSkipping < 0) {
            String messageToLogAndUser = MessagesEnum.INVALID_SKIPPING.getMessageWithArgs(
                    localeForUserMessages, Integer.toString(rowSkipping));
            log.error(messageToLogAndUser);
            throw new InvalidQueryException(messageToLogAndUser);
        }
        this.rowSkipping = rowSkipping;
    }

    /**
     * Sets the number of rows to skip when selecting only a subset of
     * the row, based on another query.
     *
     * @param originalQuery The query from which the row skipping should be taken.
     */
    public void copyRowSkipping(Query originalQuery) {
        rowSkipping = originalQuery.getRowSkipping();
    }

    /**
     * Returns true if this query has a row skipping set. A value of 0 means no
     * skipping.
     *
     * @return True if this query has a row skipping set.
     */
    public boolean hasRowSkipping() {
        return rowSkipping > 0;
    }

    /**
     * Returns the maximum number of rows to return to the caller.
     * If the caller specified this parameter, and the data table returned from
     * the data source contains more than this number of rows, then the result is
     * truncated, and the query result contains a warning with this reason.
     * If this value is set to -1 it is just ignored, but this can only be done
     * internally.
     *
     * @return The maximum number of rows to return to the caller.
     */
    public int getRowLimit() {
        return rowLimit;
    }

    /**
     * Sets the max number of rows to return to the caller.
     * If the caller specified this parameter, and the data table returned from
     * the data source contains more than this number of rows, then the result is
     * truncated, and the query result contains a warning with this reason.
     * By default, this is set to -1, which means that no limit is requested.
     * <p/>
     * If there is is an attempt to set the limit value to any negative number
     * smaller than -1 (which means no limit), then an InvalidQueryException is
     * thrown.
     *
     * @param rowLimit The max number of rows to return.
     * @throws InvalidQueryException Thrown if an invalid value is specified.
     */
    public void setRowLimit(int rowLimit) throws InvalidQueryException {
        if(rowLimit < -1) {
            String messageToLogAndUser = "Invalid value for row limit: " + rowLimit;
            log.error(messageToLogAndUser);
            throw new InvalidQueryException(messageToLogAndUser);
        }
        this.rowLimit = rowLimit;
    }

    /**
     * Sets the max number of rows to return to the caller, based on another
     * query.
     *
     * @param originalQuery The query from which the row limit should be taken.
     */
    public void copyRowLimit(Query originalQuery) {
        rowLimit = originalQuery.getRowLimit();
    }

    /**
     * Returns true if this query has a row limit set. A value of -1 means no
     * limit so in case of -1 this function returns true.
     *
     * @return True if this query has a row limit set.
     */
    public boolean hasRowLimit() {
        return rowLimit > -1;
    }

    /**
     * Returns the number of rows that should be removed from the beginning of the
     * data table.
     * Together with the row limit parameter, this enables pagination.
     * The default value is 0, (skip 0 rows) - which means to start from the
     * first row.
     *
     * @return The number of rows to skip from the beginning of the table.
     */
    public int getRowOffset() {
        return rowOffset;
    }

    /**
     * Sets the number of rows that should be removed from the beginning of the
     * data table.
     * Together with the row limit parameter, this enables pagination.
     * The default value is 0, (skip 0 rows) - which means to start from the
     * first row.
     * <p/>
     * If there is is an attempt to set the offset value to any negative number,
     * then an InvalidQueryException is thrown.
     *
     * @param rowOffset The number of rows to skip from the beginning of the
     *                  table.
     * @throws InvalidQueryException Thrown if an invalid value is specified.
     */
    public void setRowOffset(int rowOffset) throws InvalidQueryException {
        if(rowOffset < 0) {
            String messageToLogAndUser = MessagesEnum.INVALID_OFFSET.getMessageWithArgs(
                    localeForUserMessages, Integer.toString(rowOffset));
            log.error(messageToLogAndUser);
            throw new InvalidQueryException(messageToLogAndUser);
        }
        this.rowOffset = rowOffset;
    }

    /**
     * Sets the number of rows to skip, based on another query.
     *
     * @param originalQuery The query from which the row offset should be taken.
     */
    public void copyRowOffset(Query originalQuery) {
        rowOffset = originalQuery.getRowOffset();
    }

    /**
     * Returns true if this query has a row offset set. A value of 0 means no
     * offset so in case of 0 this function returns true.
     *
     * @return True if this query has a row offset set.
     */
    public boolean hasRowOffset() {
        return rowOffset > 0;
    }

    /**
     * Returns the query formatting options.
     *
     * @return The query formatting options.
     */
    public QueryFormat getUserFormatOptions() {
        return userFormatOptions;
    }

    /**
     * Sets the user formatting options map.
     *
     * @param userFormatOptions A map of patterns to column IDs.
     */
    public void setUserFormatOptions(QueryFormat userFormatOptions) {
        this.userFormatOptions = userFormatOptions;
    }

    /**
     * Returns true if this query has user format options defined.
     *
     * @return True if this query has user format options defined.
     */
    public boolean hasUserFormatOptions() {
        return (userFormatOptions != null)
                && (!userFormatOptions.getColumns().isEmpty());
    }

    /**
     * Returns the query labels.
     *
     * @return The query labels.
     */
    public QueryLabels getLabels() {
        return labels;
    }

    /**
     * Sets the query labels.
     *
     * @param labels A map of labels to column IDs.
     */
    public void setLabels(QueryLabels labels) {
        this.labels = labels;
    }

    /**
     * Returns true if this query has labels defined.
     *
     * @return True if this query has labels defined.
     */
    public boolean hasLabels() {
        return (labels != null)
                && (!labels.getColumns().isEmpty());
    }

    /**
     * Returns the options for this query.
     *
     * @return The options for this query.
     */
    public QueryOptions getOptions() {
        return options;
    }

    /**
     * Sets the required options for this query.
     *
     * @param options The required options.
     */
    public void setOptions(QueryOptions options) {
        this.options = options;
    }

    /**
     * Returns true if this query has any nondefault options set.
     *
     * @return True if this query has any nondefault options set.
     */
    public boolean hasOptions() {
        return (options != null)
                && (!options.isDefault());
    }

    /**
     * Returns true if the query is empty, i.e. has no clauses or all the clauses are empty.
     *
     * @return true if the query is empty.
     */
    public boolean isEmpty() {
        return (!hasSort() && !hasSelection() && !hasFilter() && !hasGroup() && !hasPivot()
                && !hasRowSkipping() && !hasRowLimit() && !hasRowOffset()
                && !hasUserFormatOptions() && !hasLabels() && !hasOptions());
    }


    /**
     * Sets the user locale for creating localized messages.
     *
     * @param userLocale the user locale.
     */
    public void setLocaleForUserMessages(ULocale userLocale) {
        this.localeForUserMessages = userLocale;
    }

    /**
     * Copies all information from the given query to this query.
     *
     * @param query The query to copy from.
     */
    public void copyFrom(Query query) {
        setSort(query.getSort());
        setSelection(query.getSelection());
        setFilter(query.getFilter());
        setGroup(query.getGroup());
        setPivot(query.getPivot());
        copyRowSkipping(query);
        copyRowLimit(query);
        copyRowOffset(query);
        setUserFormatOptions(query.getUserFormatOptions());
        setLabels(query.getLabels());
        setOptions(query.getOptions());
    }

    /**
     * Validates the query. Runs a sanity check on the query, verifies that there are no
     * duplicates, and that the query follows a basic set of rules required for its execution.
     * <p/>
     * Specifically, verifies the following:
     * - There are no duplicate columns in the clauses.
     * - No column appears both as a selection and as an aggregation.
     * - When aggregation is used, checks that all selected columns are valid (a column is valid if
     * it is either grouped-by, or is a scalar function column the arguments of which are all
     * valid columns).
     * - No column is both grouped-by, and aggregated in, the select clause.
     * - No column both appears as pivot, and aggregated in, the select clause.
     * - No grouping/pivoting is used when there is no aggregation in the select clause.
     * - If aggregation is used in the select clause, no column is ordered-by that is not in the
     * select clause.
     * - No column appears both as a group-by and a pivot.
     * - If pivoting is used, the order by clause does not contain aggregation columns.
     * - The order-by clause does not contain aggregation columns that were not defined in the select
     * clause.
     *
     * @throws InvalidQueryException
     */
    public void validate() throws InvalidQueryException {
        // Set up some variables.
        List<String> groupColumnIds =
                hasGroup() ? group.getColumnIds() : Lists.<String>newArrayList();
        List<AbstractColumn> groupColumns = hasGroup() ? group.getColumns() :
                Lists.<AbstractColumn>newArrayList();
        List<String> pivotColumnIds =
                hasPivot() ? pivot.getColumnIds() : Lists.<String>newArrayList();
        List<AbstractColumn> selectionColumns = hasSelection()
                ? selection.getColumns() : Lists.<AbstractColumn>newArrayList();
        List<AggregationColumn> selectionAggregated =
                hasSelection() ? selection.getAggregationColumns() :
                        Lists.<AggregationColumn>newArrayList();
        List<SimpleColumn> selectionSimple = hasSelection()
                ? selection.getSimpleColumns() : Lists.<SimpleColumn>newArrayList();
        List<ScalarFunctionColumn> selectedScalarFunctionColumns = hasSelection()
                ? selection.getScalarFunctionColumns()
                : Lists.<ScalarFunctionColumn>newArrayList();
        selectedScalarFunctionColumns.addAll(selectedScalarFunctionColumns);
        List<AbstractColumn> sortColumns = hasSort() ? sort.getColumns() :
                Lists.<AbstractColumn>newArrayList();
        List<AggregationColumn> sortAggregated = hasSort()
                ? sort.getAggregationColumns()
                : Lists.<AggregationColumn>newArrayList();

        // Check for duplicates.
        checkForDuplicates(selectionColumns, "SELECT", localeForUserMessages);
        checkForDuplicates(sortColumns, "ORDER BY", localeForUserMessages);
        checkForDuplicates(groupColumnIds, "GROUP BY", localeForUserMessages);
        checkForDuplicates(pivotColumnIds, "PIVOT", localeForUserMessages);

        // Cannot have aggregations in either group by, pivot, or where.
        if(hasGroup()) {
            for(AbstractColumn column : group.getColumns()) {
                if(!column.getAllAggregationColumns().isEmpty()) {
                    String messageToLogAndUser = MessagesEnum.CANNOT_BE_IN_GROUP_BY.getMessageWithArgs(
                            localeForUserMessages, column.toQueryString());
                    log.error(messageToLogAndUser);
                    throw new InvalidQueryException(messageToLogAndUser);
                }
            }
        }
        if(hasPivot()) {
            for(AbstractColumn column : pivot.getColumns()) {
                if(!column.getAllAggregationColumns().isEmpty()) {
                    String messageToLogAndUser = MessagesEnum.CANNOT_BE_IN_PIVOT.getMessageWithArgs(
                            localeForUserMessages, column.toQueryString());
                    log.error(messageToLogAndUser);
                    throw new InvalidQueryException(messageToLogAndUser);
                }
            }
        }
        if(hasFilter()) {
            List<AggregationColumn> filterAggregations = filter.getAggregationColumns();
            if(!filterAggregations.isEmpty()) {
                String messageToLogAndUser = MessagesEnum.CANNOT_BE_IN_WHERE.getMessageWithArgs(
                        localeForUserMessages, filterAggregations.get(0).toQueryString());
                ;
                log.error(messageToLogAndUser);
                throw new InvalidQueryException(messageToLogAndUser);
            }
        }

        // A column cannot appear both as an aggregation column and as a regular
        // column in the selection.
        for(SimpleColumn column1 : selectionSimple) {
            String id = column1.getColumnId();
            for(AggregationColumn column2 : selectionAggregated) {
                if(id.equals(column2.getAggregatedColumn().getId())) {
                    String messageToLogAndUser = MessagesEnum.SELECT_WITH_AND_WITHOUT_AGG.getMessageWithArgs(
                            localeForUserMessages, id);
                    log.error(messageToLogAndUser);
                    throw new InvalidQueryException(messageToLogAndUser);
                }
            }
        }

        // When aggregation is used, check that all selected columns are valid
        // (a column is valid if it is either grouped-by, or is a
        // scalar function column whose arguments are all valid columns).
        if(!selectionAggregated.isEmpty()) {
            for(AbstractColumn col : selectionColumns) {
                checkSelectedColumnWithGrouping(groupColumns, col);
            }
        }

        // Cannot group by a column that appears in an aggregation.
        if(hasSelection() && hasGroup()) {
            for(AggregationColumn column : selectionAggregated) {
                String id = column.getAggregatedColumn().getId();
                if(groupColumnIds.contains(id)) {
                    String messageToLogAndUser = MessagesEnum.COL_AGG_NOT_IN_SELECT.getMessageWithArgs(
                            localeForUserMessages, id);
                    log.error(messageToLogAndUser);
                    throw new InvalidQueryException(messageToLogAndUser);
                }
            }
        }

        // Cannot use grouping or pivoting when no aggregations are defined in the
        // selection.
        if(hasGroup() && selectionAggregated.isEmpty()) {
            String messageToLogAndUser = MessagesEnum.CANNOT_GROUP_WITNOUT_AGG.getMessage(
                    localeForUserMessages);
            log.error(messageToLogAndUser);
            throw new InvalidQueryException(messageToLogAndUser);
        }
        if(hasPivot() && selectionAggregated.isEmpty()) {
            String messageToLogAndUser = MessagesEnum.CANNOT_PIVOT_WITNOUT_AGG.getMessage(
                    localeForUserMessages);
            log.error(messageToLogAndUser);
            throw new InvalidQueryException(messageToLogAndUser);
        }

        // Cannot order by a column that is not in the selection when aggregations
        // are defined.
        if(hasSort() && !selectionAggregated.isEmpty()) {
            for(AbstractColumn column : sort.getColumns()) {
                String messageToLogAndUser = MessagesEnum.COL_IN_ORDER_MUST_BE_IN_SELECT.getMessageWithArgs(
                        localeForUserMessages, column.toQueryString());
                checkColumnInList(selection.getColumns(), column,
                        messageToLogAndUser);
            }
        }

        // Cannot pivot by a column that appears in an aggregation.
        if(hasPivot()) {
            for(AggregationColumn column : selectionAggregated) {
                String id = column.getAggregatedColumn().getId();
                if(pivotColumnIds.contains(id)) {
                    String messageToLogAndUser = MessagesEnum.AGG_IN_SELECT_NO_PIVOT.getMessageWithArgs(
                            localeForUserMessages, id);
                    log.error(messageToLogAndUser);
                    throw new InvalidQueryException(messageToLogAndUser);
                }
            }
        }

        // Cannot have a column appear in both group by and pivot.
        if(hasGroup() && hasPivot()) {
            for(String id : groupColumnIds) {
                if(pivotColumnIds.contains(id)) {
                    String messageToLogAndUser = MessagesEnum.NO_COL_IN_GROUP_AND_PIVOT.getMessageWithArgs(
                            localeForUserMessages, id);
                    log.error(messageToLogAndUser);
                    throw new InvalidQueryException(messageToLogAndUser);
                }
            }
        }

        // Cannot order by aggregation column when pivoting is used.
        if(hasPivot() && !sortAggregated.isEmpty()) {
            AggregationColumn column = sortAggregated.get(0);
            String messageToLogAndUser = MessagesEnum.NO_AGG_IN_ORDER_WHEN_PIVOT.getMessageWithArgs(
                    localeForUserMessages, column.getAggregatedColumn().getId());
            log.error(messageToLogAndUser);
            throw new InvalidQueryException(messageToLogAndUser);
        }

        // Cannot order by aggregation columns that weren't defined in the
        // selection.
        for(AggregationColumn column : sortAggregated) {
            String messageToLogAndUser = MessagesEnum.AGG_IN_ORDER_NOT_IN_SELECT.getMessageWithArgs(
                    localeForUserMessages, column.toQueryString());
            checkColumnInList(selectionAggregated, column, messageToLogAndUser);
        }

        Set<AbstractColumn> labelColumns = (hasLabels()
                ? labels.getColumns() : Sets.<AbstractColumn>newHashSet());
        Set<AbstractColumn> formatColumns = (hasUserFormatOptions()
                ? userFormatOptions.getColumns() : Sets.<AbstractColumn>newHashSet());

        if(hasSelection()) {
            for(AbstractColumn col : labelColumns) {
                if(!selectionColumns.contains(col)) {
                    String messageToLogAndUser = MessagesEnum.LABEL_COL_NOT_IN_SELECT.getMessageWithArgs(
                            localeForUserMessages, col.toQueryString());
                    log.error(messageToLogAndUser);
                    throw new InvalidQueryException(messageToLogAndUser);
                }
            }
            for(AbstractColumn col : formatColumns) {
                if(!selectionColumns.contains(col)) {
                    String messageToLogAndUser = MessagesEnum.FORMAT_COL_NOT_IN_SELECT.getMessageWithArgs(
                            localeForUserMessages, col.toQueryString());
                    log.error(messageToLogAndUser);
                    throw new InvalidQueryException(messageToLogAndUser);
                }
            }
        }
    }

    /**
     * Returns all simple column IDs in use in all parts of the query.
     *
     * @return All simple column IDs in use in all parts of the query.
     */
    public Set<String> getAllColumnIds() {
        Set<String> result = Sets.newHashSet();
        if(hasSelection()) {
            for(AbstractColumn col : selection.getColumns()) {
                result.addAll(col.getAllSimpleColumnIds());
            }
        }
        if(hasSort()) {
            for(AbstractColumn col : sort.getColumns()) {
                result.addAll(col.getAllSimpleColumnIds());
            }
        }
        if(hasGroup()) {
            result.addAll(getGroup().getSimpleColumnIds());
        }
        if(hasPivot()) {
            result.addAll(getPivot().getSimpleColumnIds());
        }
        if(hasFilter()) {
            result.addAll(getFilter().getAllColumnIds());
        }
        if(hasLabels()) {
            for(AbstractColumn col : labels.getColumns()) {
                result.addAll(col.getAllSimpleColumnIds());
            }
        }
        if(hasUserFormatOptions()) {
            for(AbstractColumn col : userFormatOptions.getColumns()) {
                result.addAll(col.getAllSimpleColumnIds());
            }
        }

        return result;
    }

    /**
     * Returns all aggregation columns used in all parts of this query.
     *
     * @return All aggregation columns used in all parts of this query.
     */
    public Set<AggregationColumn> getAllAggregations() {
        Set<AggregationColumn> result = Sets.newHashSet();
        if(hasSelection()) {
            result.addAll(selection.getAggregationColumns());
        }
        if(hasSort()) {
            for(AbstractColumn col : sort.getColumns()) {
                if(col instanceof AggregationColumn) {
                    result.add((AggregationColumn) col);
                }
            }
        }

        if(hasLabels()) {
            for(AbstractColumn col : labels.getColumns()) {
                if(col instanceof AggregationColumn) {
                    result.add((AggregationColumn) col);
                }
            }
        }
        if(hasUserFormatOptions()) {
            for(AbstractColumn col : userFormatOptions.getColumns()) {
                if(col instanceof AggregationColumn) {
                    result.add((AggregationColumn) col);
                }
            }
        }
        return result;
    }

    /**
     * Returns all scalar function columns in this query.
     *
     * @return All scalar function columns in this query.
     */
    public Set<ScalarFunctionColumn> getAllScalarFunctionsColumns() {
        Set<ScalarFunctionColumn> mentionedScalarFunctionColumns =
                Sets.newHashSet();
        if(hasSelection()) {
            mentionedScalarFunctionColumns.addAll(
                    selection.getScalarFunctionColumns());
        }
        if(hasFilter()) {
            mentionedScalarFunctionColumns.addAll(filter.getScalarFunctionColumns());
        }
        if(hasGroup()) {
            mentionedScalarFunctionColumns.addAll(group.getScalarFunctionColumns());
        }
        if(hasPivot()) {
            mentionedScalarFunctionColumns.addAll(pivot.getScalarFunctionColumns());
        }
        if(hasSort()) {
            mentionedScalarFunctionColumns.addAll(sort.getScalarFunctionColumns());
        }
        if(hasLabels()) {
            mentionedScalarFunctionColumns.addAll(labels.getScalarFunctionColumns());
        }
        if(hasUserFormatOptions()) {
            mentionedScalarFunctionColumns.addAll(userFormatOptions.getScalarFunctionColumns());
        }
        return mentionedScalarFunctionColumns;
    }


    /**
     * Checks that the given column is valid, i.e., is in the given list of
     * columns, or is a scalar function column and all its inner columns are
     * valid (i.e., in the list).
     *
     * @param columns             The given list of columns.
     * @param column              The given column.
     * @param messageToLogAndUser The error message for the exception that is
     *                            thrown when the column is not in the list.
     * @throws InvalidQueryException Thrown when the column is invalid.
     */
    private void checkColumnInList(List<? extends AbstractColumn> columns,
                                   AbstractColumn column, String messageToLogAndUser)
            throws InvalidQueryException {
        if(columns.contains(column)) {
            return;
        }
        else if(column instanceof ScalarFunctionColumn) {
            List<AbstractColumn> innerColumns =
                    ((ScalarFunctionColumn) column).getColumns();
            for(AbstractColumn innerColumn : innerColumns) {
                checkColumnInList(columns, innerColumn, messageToLogAndUser);
            }
        }
        else {
            log.error(messageToLogAndUser);
            throw new InvalidQueryException(messageToLogAndUser);
        }
    }

    /**
     * Checks the selection of the given column is valid, given the group
     * columns. A column is valid in this sense if it is either grouped-by, or is
     * a scalar function column whose arguments are all valid columns.
     *
     * @param groupColumns The group columns.
     * @param col          The selected column.
     * @throws InvalidQueryException Thrown when the given column is a simple
     *                               column that is not grouped by.
     */
    private void checkSelectedColumnWithGrouping(
            List<AbstractColumn> groupColumns, AbstractColumn col)
            throws InvalidQueryException {
        if(col instanceof SimpleColumn) {
            if(!groupColumns.contains(col)) {
                String messageToLogAndUser = MessagesEnum.ADD_COL_TO_GROUP_BY_OR_AGG.getMessageWithArgs(
                        localeForUserMessages, col.getId());
                log.error(messageToLogAndUser);
                throw new InvalidQueryException(messageToLogAndUser);
            }
        }
        else if(col instanceof ScalarFunctionColumn) {
            // A selected scalar function column is valid if it is grouped by, or if
            // its inner columns are all valid.
            if(!groupColumns.contains(col)) {
                List<AbstractColumn> innerColumns =
                        ((ScalarFunctionColumn) col).getColumns();
                for(AbstractColumn innerColumn : innerColumns) {
                    checkSelectedColumnWithGrouping(groupColumns, innerColumn);
                }
            }
        }
        // An aggregation column is always valid (the inner aggregated column
        // validity is checked elsewhere).
    }

    @Override
    public int hashCode() {
        final int prime = 37;
        int result = 1;
        result = prime * result + ((filter == null) ? 0 : filter.hashCode());
        result = prime * result + ((group == null) ? 0 : group.hashCode());
        result = prime * result + ((labels == null) ? 0 : labels.hashCode());
        result = prime * result + ((options == null) ? 0 : options.hashCode());
        result = prime * result + ((pivot == null) ? 0 : pivot.hashCode());
        result = prime * result + rowSkipping;
        result = prime * result + rowLimit;
        result = prime * result + rowOffset;
        result = prime * result + ((selection == null) ? 0 : selection.hashCode());
        result = prime * result + ((sort == null) ? 0 : sort.hashCode());
        result = prime * result + ((userFormatOptions == null) ? 0 : userFormatOptions.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if(this == obj) {
            return true;
        }
        if(obj == null) {
            return false;
        }
        if(getClass() != obj.getClass()) {
            return false;
        }
        Query other = (Query) obj;
        if(filter == null) {
            if(other.filter != null) {
                return false;
            }
        }
        else if(!filter.equals(other.filter)) {
            return false;
        }
        if(group == null) {
            if(other.group != null) {
                return false;
            }
        }
        else if(!group.equals(other.group)) {
            return false;
        }
        if(labels == null) {
            if(other.labels != null) {
                return false;
            }
        }
        else if(!labels.equals(other.labels)) {
            return false;
        }
        if(options == null) {
            if(other.options != null) {
                return false;
            }
        }
        else if(!options.equals(other.options)) {
            return false;
        }
        if(pivot == null) {
            if(other.pivot != null) {
                return false;
            }
        }
        else if(!pivot.equals(other.pivot)) {
            return false;
        }
        if(rowSkipping != other.rowSkipping) {
            return false;
        }
        if(rowLimit != other.rowLimit) {
            return false;
        }
        if(rowOffset != other.rowOffset) {
            return false;
        }
        if(selection == null) {
            if(other.selection != null) {
                return false;
            }
        }
        else if(!selection.equals(other.selection)) {
            return false;
        }
        if(sort == null) {
            if(other.sort != null) {
                return false;
            }
        }
        else if(!sort.equals(other.sort)) {
            return false;
        }
        if(userFormatOptions == null) {
            if(other.userFormatOptions != null) {
                return false;
            }
        }
        else if(!userFormatOptions.equals(other.userFormatOptions)) {
            return false;
        }
        return true;
    }

    /**
     * Creates a comma separated string of the query strings of the given columns.
     *
     * @param l The list of columns.
     * @return A comma separated string of the query strings of the given columns.
     */
    /* package */
    static String columnListToQueryString(List<AbstractColumn> l) {
        StrBuilder builder = new StrBuilder();
        List<String> stringList = Lists.newArrayList();
        for(AbstractColumn col : l) {
            stringList.add(col.toQueryString());
        }
        builder.appendWithSeparators(stringList, ", ");
        return builder.toString();
    }

    /**
     * Creates a query-language representation of the given string, i.e., delimits it with either
     * double-quotes (") or single-quotes ('). Throws a runtime exception if the given string
     * contains both double-quotes and single-quotes and so cannot be expressed in the query
     * language.
     *
     * @param s The string to create a query-language representation for.
     * @return The query-language representation of the given string.
     */
    /* package */
    static String stringToQueryStringLiteral(String s) {
        if(s.contains("\"")) {
            if(s.contains("'")) {
                throw new RuntimeException("Cannot represent string that contains both double-quotes (\") "
                        + " and single quotes (').");
            }
            else {
                return "'" + s + "'";
            }
        }
        else {
            return "\"" + s + "\"";
        }
    }

    /**
     * Returns a string that when fed to the query parser will yield an identical Query.
     * Used mainly for debugging purposes.
     *
     * @return The query string.
     */
    public String toQueryString() {
        List<String> clauses = Lists.newArrayList();
        if(hasSelection()) {
            clauses.add("SELECT " + selection.toQueryString());
        }
        if(hasFilter()) {
            clauses.add("WHERE " + filter.toQueryString());
        }
        if(hasGroup()) {
            clauses.add("GROUP BY " + group.toQueryString());
        }
        if(hasPivot()) {
            clauses.add("PIVOT " + pivot.toQueryString());
        }
        if(hasSort()) {
            clauses.add("ORDER BY " + sort.toQueryString());
        }
        if(hasRowSkipping()) {
            clauses.add("SKIPPING " + rowSkipping);
        }
        if(hasRowLimit()) {
            clauses.add("LIMIT " + rowLimit);
        }
        if(hasRowOffset()) {
            clauses.add("OFFSET " + rowOffset);
        }
        if(hasLabels()) {
            clauses.add("LABEL " + labels.toQueryString());
        }
        if(hasUserFormatOptions()) {
            clauses.add("FORMAT " + userFormatOptions.toQueryString());
        }
        if(hasOptions()) {
            clauses.add("OPTIONS " + options.toQueryString());
        }
        StrBuilder result = new StrBuilder();
        result.appendWithSeparators(clauses, " ");
        return result.toString();
    }
}
TOP

Related Classes of com.google.visualization.datasource.query.Query

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.