// 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;
import com.google.visualization.datasource.base.DataSourceException;
import com.google.visualization.datasource.base.DataSourceParameters;
import com.google.visualization.datasource.base.InvalidQueryException;
import com.google.visualization.datasource.base.LocaleUtil;
import com.google.visualization.datasource.base.MessagesEnum;
import com.google.visualization.datasource.base.OutputType;
import com.google.visualization.datasource.base.ReasonType;
import com.google.visualization.datasource.base.ResponseStatus;
import com.google.visualization.datasource.base.StatusType;
import com.google.visualization.datasource.datatable.DataTable;
import com.google.visualization.datasource.query.AggregationColumn;
import com.google.visualization.datasource.query.Query;
import com.google.visualization.datasource.query.ScalarFunctionColumn;
import com.google.visualization.datasource.query.engine.QueryEngine;
import com.google.visualization.datasource.query.parser.QueryBuilder;
import com.google.visualization.datasource.render.CsvRenderer;
import com.google.visualization.datasource.render.HtmlRenderer;
import com.google.visualization.datasource.render.JsonRenderer;
import com.ibm.icu.util.ULocale;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.util.Locale;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* A Helper class providing convenience functions for serving data source requests.
*
* The class enables replying to a data source request with a single method which encompasses
* all the request processing - <code>executeDataSourceServletFlow</code>.
* To enable users to change the default flow all the basic operations (such as: query parsing,
* data table creation, query execution, and response creation) are also exposed.
*
* @author Yaniv S.
*/
public class DataSourceHelper {
/**
* The log used throughout the data source library.
*/
private static final Log log = LogFactory.getLog(DataSourceHelper.class.getName());
/**
* The name of the http request parameter that indicates the requested locale.
*/
/* package */ static final String LOCALE_REQUEST_PARAMETER = "hl";
/**
* A private constructor for this Singleton.
*/
private DataSourceHelper() {}
/**
* Executes the default data source servlet flow.
* Assumes restricted access mode.
* @see <code>executeDataSourceServletFlow(HttpServletRequest req, HttpServletResponse resp,
* DataTableGenerator dtGenerator, boolean isRestrictedAccessMode)</code>
*
* @param req The HttpServletRequest.
* @param resp The HttpServletResponse.
* @param dtGenerator An implementation of {@link DataTableGenerator} interface.
*
* @throws IOException In case of I/O errors.
*/
public static void executeDataSourceServletFlow(HttpServletRequest req, HttpServletResponse resp,
DataTableGenerator dtGenerator) throws IOException {
executeDataSourceServletFlow(req, resp, dtGenerator, true);
}
/**
* Executes the default data source servlet flow.
*
* The default flow is as follows:
* - Parse the request parameters.
* - Verify access is approved (for restricted access mode only).
* - Split the query.
* - Generate the data-table using the data-table generator.
* - Run the completion query.
* - Set the servlet response.
*
* Usage note : this function executes the same flow provided to Servlets that inherit
* <code>DataSourceServlet</code>.
* Use this function when the default flow is required but <code>DataSourceServlet</code>
* cannot be inherited (e.g., your servlet already inherits from anther class, or not in a
* servlet context).
*
* @param req The HttpServletRequest.
* @param resp The HttpServletResponse.
* @param dtGenerator An implementation of {@link DataTableGenerator} interface.
* @param isRestrictedAccessMode Indicates whether the server should serve trusted domains only.
* Currently this translates to serving only requests from the same domain.
*
* @throws IOException In case of I/O errors.
*/
public static void executeDataSourceServletFlow(HttpServletRequest req, HttpServletResponse resp,
DataTableGenerator dtGenerator, boolean isRestrictedAccessMode) throws IOException {
// Extract the data source request parameters.
DataSourceRequest dsRequest = null;
try {
dsRequest = new DataSourceRequest(req);
if (isRestrictedAccessMode) {
// Verify that the request is approved for access.
DataSourceHelper.verifyAccessApproved(dsRequest);
}
// Split the query.
QueryPair query = DataSourceHelper.splitQuery(dsRequest.getQuery(),
dtGenerator.getCapabilities());
// Generate the data table.
DataTable dataTable = dtGenerator.generateDataTable(query.getDataSourceQuery(), req);
// Apply the completion query to the data table.
DataTable newDataTable = DataSourceHelper.applyQuery(query.getCompletionQuery(), dataTable,
dsRequest.getUserLocale());
// Set the response.
setServletResponse(newDataTable, dsRequest, resp);
} catch (DataSourceException e) {
if (dsRequest != null) {
setServletErrorResponse(e, dsRequest, resp);
} else {
DataSourceHelper.setServletErrorResponse(e, req, resp);
}
} catch (RuntimeException e) {
log.error("A runtime exception has occured", e);
ResponseStatus status = new ResponseStatus(StatusType.ERROR, ReasonType.INTERNAL_ERROR,
e.getMessage());
if (dsRequest == null) {
dsRequest = DataSourceRequest.getDefaultDataSourceRequest(req);
}
DataSourceHelper.setServletErrorResponse(status, dsRequest, resp);
}
}
/**
* Checks that the given request is sent from the same domain as that of the server.
*
* @param req The data source request.
*
* @throws DataSourceException If the access for this request is denied.
*/
public static void verifyAccessApproved(DataSourceRequest req) throws DataSourceException {
// The library requires the request to be same origin for JSON and JSONP.
// Check for (!csv && !html && !tsv-excel) to make sure any output type
// added in the future will be restricted to the same domain by default.
OutputType outType = req.getDataSourceParameters().getOutputType();
if (outType != OutputType.CSV && outType != OutputType.TSV_EXCEL
&& outType != OutputType.HTML && !req.isSameOrigin()) {
throw new DataSourceException(ReasonType.ACCESS_DENIED,
"Unauthorized request. Cross domain requests are not supported.");
}
}
// -------------------------- Servlet helper methods --------------------------------------------
/**
* Sets the response on the <code>HttpServletResponse</code> by creating a response message
* for the given <code>DataTable</code> and sets it on the <code>HttpServletResponse</code>.
*
* @param dataTable The data table.
* @param dataSourceRequest The data source request.
* @param res The http servlet response.
*
* @throws IOException In case an error happened trying to write the response to the servlet.
*/
public static void setServletResponse(DataTable dataTable, DataSourceRequest dataSourceRequest,
HttpServletResponse res) throws IOException {
String responseMessage = generateResponse(dataTable, dataSourceRequest);
setServletResponse(responseMessage, dataSourceRequest, res);
}
/**
* Sets the given response string on the <code>HttpServletResponse</code>.
*
* @param responseMessage The response message.
* @param dataSourceRequest The data source request.
* @param res The HTTP response.
*
* @throws IOException In case an error happened trying to write to the servlet response.
*/
public static void setServletResponse(String responseMessage,
DataSourceRequest dataSourceRequest, HttpServletResponse res) throws IOException {
DataSourceParameters dataSourceParameters = dataSourceRequest.getDataSourceParameters();
ResponseWriter.setServletResponse(responseMessage, dataSourceParameters, res);
}
/**
* Sets the HTTP servlet response in case of an error.
*
* @param dataSourceException The data source exception.
* @param dataSourceRequest The data source request.
* @param res The http servlet response.
*
* @throws IOException In case an error happened trying to write the response to the servlet.
*/
public static void setServletErrorResponse(DataSourceException dataSourceException,
DataSourceRequest dataSourceRequest, HttpServletResponse res) throws IOException {
String responseMessage = generateErrorResponse(dataSourceException, dataSourceRequest);
setServletResponse(responseMessage, dataSourceRequest, res);
}
/**
* Sets the HTTP servlet response in case of an error.
*
* @param responseStatus The response status.
* @param dataSourceRequest The data source request.
* @param res The http servlet response.
*
* @throws IOException In case an error happened trying to write the response to the servlet.
*/
public static void setServletErrorResponse(ResponseStatus responseStatus,
DataSourceRequest dataSourceRequest, HttpServletResponse res) throws IOException {
String responseMessage = generateErrorResponse(responseStatus, dataSourceRequest);
setServletResponse(responseMessage, dataSourceRequest, res);
}
/**
* Sets the HTTP servlet response in case of an error.
*
* Gets an <code>HttpRequest</code> parameter instead of a <code>DataSourceRequest</code>.
* Use this when <code>DataSourceRequest</code> is not available, for example, if
* <code>DataSourceRequest</code> constructor failed.
*
* @param dataSourceException The data source exception.
* @param req The http servlet request.
* @param res The http servlet response.
*
* @throws IOException In case an error happened trying to write the response to the servlet.
*/
public static void setServletErrorResponse(DataSourceException dataSourceException,
HttpServletRequest req, HttpServletResponse res) throws IOException {
DataSourceRequest dataSourceRequest = DataSourceRequest.getDefaultDataSourceRequest(req);
setServletErrorResponse(dataSourceException, dataSourceRequest, res);
}
// -------------------- Response message helper methods. ----------------------------------------
/**
* Generates a string response for the given <code>DataTable</code>.
*
* @param dataTable The data table.
* @param dataSourceRequest The data source request.
*
* @return The response string.
*/
public static String generateResponse(DataTable dataTable, DataSourceRequest dataSourceRequest) {
CharSequence response;
ResponseStatus responseStatus = null;
if (!dataTable.getWarnings().isEmpty()) {
responseStatus = new ResponseStatus(StatusType.WARNING);
}
switch (dataSourceRequest.getDataSourceParameters().getOutputType()) {
case CSV:
response = CsvRenderer.renderDataTable(dataTable, dataSourceRequest.getUserLocale(), ",");
break;
case TSV_EXCEL:
response = CsvRenderer.renderDataTable(dataTable, dataSourceRequest.getUserLocale(), "\t");
break;
case HTML:
response = HtmlRenderer.renderDataTable(dataTable, dataSourceRequest.getUserLocale());
break;
case JSONP:
// Appending a comment to the response to prevent the first characters to be the
// response handler which is not controlled by the server.
response = "// Data table response\n" + JsonRenderer.renderJsonResponse(
dataSourceRequest.getDataSourceParameters(), responseStatus, dataTable);
break;
case JSON:
response = JsonRenderer.renderJsonResponse(
dataSourceRequest.getDataSourceParameters(), responseStatus, dataTable);
break;
default:
// This should never happen.
throw new RuntimeException("Unhandled output type.");
}
return response.toString();
}
/**
* Generates an error response string for the given {@link DataSourceException}.
* Receives an exception, and renders it to an error response according to the
*{@link OutputType} specified in the {@link DataSourceRequest}.
*
* Note: modifies the response status to make links clickable in cases where the reason type is
* {@link ReasonType#USER_NOT_AUTHENTICATED}. If this is not required call generateErrorResponse
* directly with a {@link ResponseStatus}.
*
* @param dse The data source exception.
* @param dsRequest The DataSourceRequest.
*
* @return The error response string.
*
* @throws IOException In case if I/O errors.
*/
public static String generateErrorResponse(DataSourceException dse, DataSourceRequest dsRequest)
throws IOException {
ResponseStatus responseStatus = ResponseStatus.createResponseStatus(dse);
responseStatus = ResponseStatus.getModifiedResponseStatus(responseStatus);
return generateErrorResponse(responseStatus, dsRequest);
}
/**
* Generates an error response string for the given <code>ResponseStatus</code>.
* Render the <code>ResponseStatus</code> to an error response according to the
* <code>OutputType</code> specified in the <code>DataSourceRequest</code>.
*
* @param responseStatus The response status.
* @param dsRequest The DataSourceRequest.
*
* @return The error response string.
*
* @throws IOException In case if I/O errors.
*/
public static String generateErrorResponse(ResponseStatus responseStatus,
DataSourceRequest dsRequest) throws IOException {
DataSourceParameters dsParameters = dsRequest.getDataSourceParameters();
CharSequence response;
switch (dsParameters.getOutputType()) {
case CSV:
case TSV_EXCEL:
response = CsvRenderer.renderCsvError(responseStatus);
break;
case HTML:
response = HtmlRenderer.renderHtmlError(responseStatus);
break;
case JSONP:
response = JsonRenderer.renderJsonResponse(dsParameters, responseStatus, null);
break;
case JSON:
response = JsonRenderer.renderJsonResponse(dsParameters, responseStatus, null);
break;
default:
// This should never happen.
throw new RuntimeException("Unhandled output type.");
}
return response.toString();
}
// -------------------------- Query helper methods ----------------------------------------------
/** @see #parseQuery(String, ULocale)*/
public static Query parseQuery(String queryString) throws InvalidQueryException {
return parseQuery(queryString, null);
}
/**
* Parses a query string (e.g., 'select A,B pivot B') and creates a Query object.
* Throws an exception if the query is invalid.
*
* @param queryString The query string.
* @param locale The user locale.
*
* @return The parsed query object.
*
* @throws InvalidQueryException If the query is invalid.
*/
public static Query parseQuery(String queryString, ULocale userLocale)
throws InvalidQueryException {
QueryBuilder queryBuilder = QueryBuilder.getInstance();
Query query = queryBuilder.parseQuery(queryString, userLocale);
return query;
}
/**
* Applies the given <code>Query</code> on the given <code>DataTable</code> and returns the
* resulting <code>DataTable</code>. This method may change the given DataTable.
* Error messages produced by this method will be localized according to the passed locale
* unless the specified {@code DataTable} has a non null locale.
*
* @param query The query object.
* @param dataTable The data table on which to apply the query.
* @param locale The user locale for the current request.
*
* @return The data table result of the query execution over the given data table.
*
* @throws InvalidQueryException If the query is invalid.
* @throws DataSourceException If the data source cannot execute the query.
*/
public static DataTable applyQuery(Query query, DataTable dataTable, ULocale locale)
throws InvalidQueryException, DataSourceException {
dataTable.setLocaleForUserMessages(locale);
validateQueryAgainstColumnStructure(query, dataTable);
dataTable = QueryEngine.executeQuery(query, dataTable, locale);
dataTable.setLocaleForUserMessages(locale);
return dataTable;
}
/**
* Splits the <code>Query</code> object into two queries according to the declared data source
* capabilities: data source query and completion query.
*
* The data source query is executed first by the data source itself. Afterward, the
* <code>QueryEngine</code> executes the completion query over the resulting data table.
*
* @param query The query to split.
* @param capabilities The declared capabilities of the data source.
*
* @return A QueryPair object.
*
* @throws DataSourceException If the query cannot be split.
*/
public static QueryPair splitQuery(Query query, Capabilities capabilities)
throws DataSourceException {
return QuerySplitter.splitQuery(query, capabilities);
}
/**
* Checks that the query is valid against the structure of the data table.
* A query is invalid if:
* <ol>
* <li> The query references column ids that don't exist in the data table.
* <li> The query contains calculated columns operations (i.e., scalar function, aggregations)
* that do not match the relevant columns type.
* </ol>
*
* Note: does NOT validate the query itself, i.e. errors like "SELECT a, a" or
* "SELECT a GROUP BY a" will not be caught. These kind of errors should be checked elsewhere
* (preferably by the <code>Query.validate()</code> method).
*
* @param query The query to check for validity.
* @param dataTable The data table against which to validate. Only the columns are used.
*
* @throws InvalidQueryException Thrown if the query is found to be invalid
* against the given data table.
*/
public static void validateQueryAgainstColumnStructure(Query query, DataTable dataTable)
throws InvalidQueryException {
// Check that all the simple columns exist in the table (including the
// simple columns inside aggregation and scalar-function columns)
Set<String> mentionedColumnIds = query.getAllColumnIds();
for (String columnId : mentionedColumnIds) {
if (!dataTable.containsColumn(columnId)) {
String messageToLogAndUser = MessagesEnum.NO_COLUMN.getMessageWithArgs(
dataTable.getLocaleForUserMessages(), columnId);
log.error(messageToLogAndUser);
throw new InvalidQueryException(messageToLogAndUser);
}
}
// Check that all aggregation columns are valid (i.e., the aggregation type
// matches the columns type).
Set<AggregationColumn> mentionedAggregations = query.getAllAggregations();
for (AggregationColumn agg : mentionedAggregations) {
try {
agg.validateColumn(dataTable);
} catch (RuntimeException e) {
log.error("A runtime exception has occured", e);
throw new InvalidQueryException(e.getMessage());
}
}
// Check that all scalar function columns are valid. (i.e., the scalar
// function matches the columns types).
Set<ScalarFunctionColumn> mentionedScalarFunctionColumns =
query.getAllScalarFunctionsColumns();
for (ScalarFunctionColumn col : mentionedScalarFunctionColumns) {
col.validateColumn(dataTable);
}
}
/**
* Get the locale from the given request.
*
* @param req The http serlvet request
*
* @return The locale for the given request.
*/
public static ULocale getLocaleFromRequest(HttpServletRequest req) {
Locale locale;
String requestLocale = req.getParameter(LOCALE_REQUEST_PARAMETER);
if (requestLocale != null) {
// Try to take the locale from the 'hl' parameter in the request.
locale = LocaleUtil.getLocaleFromLocaleString(requestLocale);
} else {
// Else, take the browser locale.
locale = req.getLocale();
}
return ULocale.forLocale(locale);
}
}