// 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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Set;
import com.google.visualization.datasource.base.DataSourceException;
import com.google.visualization.datasource.base.InvalidQueryException;
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.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.render.Renderer;
import com.google.visualization.datasource.render.RendererFactory;
import com.ibm.icu.util.ULocale;
/**
* A Helper class providing convenience functions for serving data source requests.
* <p/>
* 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 DataSourceService {
private static final Log log = LogFactory.getLog(DataSourceService.class);
/**
* Executes the default data source servlet flow.
*
* @param request The HttpServletRequest.
* @param response The HttpServletResponse.
* @param generator An implementation of {@link com.google.visualization.datasource.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.
* @throws DataSourceException Invalid parameters
*/
public void execute(HttpServletRequest request, HttpServletResponse response,
DataTableGenerator generator,
boolean isRestrictedAccessMode)
throws IOException, DataSourceException {
// Extract the data source request parameters.
this.execute(request, response, new DataSourceRequest(request), generator, isRestrictedAccessMode);
}
/**
* Executes the default data source servlet flow. Create renderer based on data source parameters.
*
* @param request The HttpServletRequest.
* @param response The HttpServletResponse.
* @param dataSourceRequest Request parameters
* @param generator An implementation of {@link com.google.visualization.datasource.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 void execute(HttpServletRequest request, HttpServletResponse response,
DataSourceRequest dataSourceRequest,
DataTableGenerator generator,
boolean isRestrictedAccessMode)
throws IOException {
final Renderer renderer = RendererFactory.create(dataSourceRequest.getDataSourceParameters().getOutputType());
this.execute(request, response, dataSourceRequest, generator, renderer, isRestrictedAccessMode);
}
/**
* Executes the default data source servlet flow.
* <p/>
* 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.
* <p/>
* 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 request The HttpServletRequest.
* @param response The HttpServletResponse.
* @param dataSourceRequest Request parameters
* @param generator An implementation of {@link DataTableGenerator} interface.
* @param renderer Output format
* @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 void execute(HttpServletRequest request, HttpServletResponse response,
DataSourceRequest dataSourceRequest,
DataTableGenerator generator,
Renderer renderer,
boolean isRestrictedAccessMode)
throws IOException {
try {
if(isRestrictedAccessMode) {
// Verify that the request is approved for access.
this.verifyAccessApproved(dataSourceRequest);
}
// Split the query.
QueryPair query = this.splitQuery(dataSourceRequest.getQuery(),
generator.getCapabilities());
// Generate the data table.
DataTable dataTable = generator.create(query.getDataSourceQuery(), request);
// Apply the completion query to the data table.
DataTable newDataTable = this.applyQuery(query.getCompletionQuery(), dataTable,
dataSourceRequest.getUserLocale());
// Set the response.
CharSequence output = renderer.render(dataSourceRequest, newDataTable);
renderer.setHeaders(dataSourceRequest, response);
OutputStream outputStream = response.getOutputStream();
outputStream.write(output.toString().getBytes(renderer.getCharset()));
}
catch(DataSourceException e) {
OutputStream outputStream = response.getOutputStream();
outputStream.write(renderer.error(dataSourceRequest,
new ResponseStatus(e)).toString().getBytes(renderer.getCharset()));
}
}
/**
* 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.
*/
protected 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.");
}
}
// -------------------------- Query helper methods ----------------------------------------------
/**
* 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.
*/
protected DataTable applyQuery(Query query, DataTable dataTable, ULocale locale)
throws DataSourceException {
dataTable.setLocaleForUserMessages(locale);
this.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.
* <p/>
* 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.
*/
protected 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>
* <p/>
* 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.
*/
protected 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);
}
}
}