// 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 javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
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.TypeMismatchException;
import com.google.visualization.datasource.datatable.ColumnDescription;
import com.google.visualization.datasource.datatable.DataTable;
import com.google.visualization.datasource.datatable.TableRow;
import com.google.visualization.datasource.datatable.value.BooleanValue;
import com.google.visualization.datasource.datatable.value.NumberValue;
import com.google.visualization.datasource.datatable.value.TextValue;
import com.google.visualization.datasource.datatable.value.ValueType;
import com.google.visualization.datasource.query.Query;
import com.google.visualization.datasource.render.CsvRenderer;
import com.google.visualization.datasource.render.JsonRenderer;
import com.google.visualization.datasource.render.JsonpRenderer;
import com.google.visualization.datasource.render.TsvRenderer;
import com.ibm.icu.util.ULocale;
import junit.framework.TestCase;
import static org.easymock.EasyMock.*;
/**
* Unit test for Helper.
*
* @author Yaniv S.
*/
public class DataSourceServiceTest extends TestCase {
public void testApplyQuery() throws DataSourceException,
TypeMismatchException {
DataTable data = createData();
DataSourceService helper = new DataSourceService();
// Test select.
DataTable result = helper.applyQuery(DataSourceRequest.parseQuery("select population"),
data, ULocale.US);
assertEquals(1, result.getNumberOfColumns());
assertEquals(4, result.getNumberOfRows());
assertEquals(new NumberValue(300), result.getRow(1).getCell(0).getValue());
data = createData();
// Test where.
result = helper.applyQuery(DataSourceRequest.parseQuery(
"select name,vegeterian where population > 100"), data, ULocale.US);
assertEquals(2, result.getNumberOfColumns());
assertEquals(1, result.getNumberOfRows());
assertEquals(new TextValue("Sloth"), result.getRow(0).getCell(0).getValue());
assertEquals(BooleanValue.TRUE, result.getRow(0).getCell(1).getValue());
data = createData();
// Test group by.
result = helper.applyQuery(DataSourceRequest.parseQuery(
"select vegeterian,sum(population) group by vegeterian"), data, ULocale.US);
assertEquals(2, result.getNumberOfColumns());
assertEquals(2, result.getNumberOfRows());
assertEquals(BooleanValue.FALSE, result.getRow(0).getCell(0).getValue());
assertEquals(new NumberValue(130), result.getRow(0).getCell(1).getValue());
data = createData();
// Test pivot.
result = helper.applyQuery(DataSourceRequest.parseQuery(
"select sum(population) pivot vegeterian"), data, ULocale.US);
assertEquals(2, result.getNumberOfColumns());
assertEquals(1, result.getNumberOfRows());
assertEquals("false", result.getColumnDescription(0).getLabel());
assertEquals(new NumberValue(130), result.getRow(0).getCell(0).getValue());
data = createData();
// Test order by.
result = helper.applyQuery(DataSourceRequest.parseQuery(
"select name order by population"), data, ULocale.US);
assertEquals(1, result.getNumberOfColumns());
assertEquals(4, result.getNumberOfRows());
assertEquals(new TextValue("Leopard"), result.getRow(0).getCell(0).getValue());
assertEquals(new TextValue("Tiger"), result.getRow(1).getCell(0).getValue());
assertEquals(new TextValue("Aye-aye"), result.getRow(2).getCell(0).getValue());
assertEquals(new TextValue("Sloth"), result.getRow(3).getCell(0).getValue());
data = createData();
// Test limit and offset.
result = helper.applyQuery(DataSourceRequest.parseQuery("limit 1 offset 1"), data,
ULocale.US);
assertEquals(4, result.getNumberOfColumns());
assertEquals(1, result.getNumberOfRows());
assertEquals(new TextValue("Sloth"), result.getRow(0).getCell(0).getValue());
data = createData();
// Test label and format.
result = helper.applyQuery(DataSourceRequest.parseQuery(
"label population 'Population size (thousands)' format population \"'$'#'k'\""), data,
ULocale.US);
assertEquals(4, result.getNumberOfColumns());
assertEquals(4, result.getNumberOfRows(), 4);
assertEquals("Population size (thousands)",
result.getColumnDescription("population").getLabel());
int populationIndex = result.getColumnIndex("population");
assertEquals("$100k", result.getRow(0).getCell(populationIndex).getFormattedValue());
// Test that validation can fail
try {
DataSourceRequest.parseQuery("select min(min(a))");
fail();
}
catch(InvalidQueryException e) {
// Do nothing.
}
// Test that validation against the table description can fail.
Query q = DataSourceRequest.parseQuery("select avg(name) group by link");
try {
helper.applyQuery(q, data, ULocale.US);
fail();
}
catch(InvalidQueryException e) {
// Do nothing.
}
}
private DataTable createData() throws TypeMismatchException {
DataTable data = new DataTable();
ArrayList<ColumnDescription> cd = new ArrayList<ColumnDescription>();
cd.add(new ColumnDescription("name", ValueType.TEXT, "Animal name"));
cd.add(new ColumnDescription("link", ValueType.TEXT, "Link to wikipedia"));
cd.add(new ColumnDescription("population", ValueType.NUMBER, "Population size"));
cd.add(new ColumnDescription("vegeterian", ValueType.BOOLEAN, "Vegeterian?"));
data.addColumns(cd);
// Fill the data-table. addRow() needs to be implemented as taking unknown
// number of arguments for this to work.
data.addRowFromValues("Aye-aye", "http://en.wikipedia.org/wiki/Aye-aye", 100, true);
data.addRowFromValues("Sloth", "http://en.wikipedia.org/wiki/Sloth", 300, true);
data.addRowFromValues("Leopard", "http://en.wikipedia.org/wiki/Leopard", 50, false);
data.addRowFromValues("Tiger", "http://en.wikipedia.org/wiki/Tiger", 80, false);
return data;
}
public void testGenerateResponse() throws DataSourceException {
// Check with simple data table and simple data source parameters.
DataTable dataTable = new DataTable();
dataTable.addColumn(new ColumnDescription("col1", ValueType.NUMBER, "column1"));
dataTable.addColumn(new ColumnDescription("col2", ValueType.BOOLEAN, "column2"));
dataTable.addColumn(new ColumnDescription("col3", ValueType.TEXT, "column3"));
TableRow tableRow = new TableRow();
tableRow.addCell(7);
tableRow.addCell(false);
tableRow.addCell("Why?");
dataTable.addRow(tableRow);
JsonRenderer json = new JsonRenderer();
DataSourceRequest dataSourceRequest = new DataSourceRequest();
assertEquals(
"{\"version\":\"0.6\",\"status\":\"ok\",\"sig\":\"1548939605\","
+ "\"table\":{\"cols\":[{\"id\":\"col1\",\"label\":\"column1\","
+ "\"type\":\"number\",\"pattern\":\"\"},"
+ "{\"id\":\"col2\",\"label\":\"column2\",\"type\":\"boolean\",\"pattern\":\"\"},"
+ "{\"id\":\"col3\",\"label\":\"column3\",\"type\":\"string\",\"pattern\":\"\"}],"
+ "\"rows\":[{\"c\":[{\"v\":7.0},{\"v\":false},{\"v\":\"Why?\"}]}]}}",
json.render(dataSourceRequest, dataTable));
// With reqId:666;
dataSourceRequest = new DataSourceRequest(
new DataSourceParameters("reqId:666"));
assertEquals(
"{\"version\":\"0.6\",\"reqId\":\"666\",\"status\":\"ok\",\"sig\":\"1548939605\","
+ "\"table\":{\"cols\":[{\"id\":\"col1\",\"label\":\"column1\","
+ "\"type\":\"number\",\"pattern\":\"\"},"
+ "{\"id\":\"col2\",\"label\":\"column2\",\"type\":\"boolean\",\"pattern\":\"\"},"
+ "{\"id\":\"col3\",\"label\":\"column3\",\"type\":\"string\",\"pattern\":\"\"}],"
+ "\"rows\":[{\"c\":[{\"v\":7.0},{\"v\":false},{\"v\":\"Why?\"}]}]}}",
json.render(dataSourceRequest, dataTable));
// With out:json;
dataSourceRequest = new DataSourceRequest(
new DataSourceParameters("out:json"));
assertEquals(
"{\"version\":\"0.6\",\"status\":\"ok\",\"sig\":\"1548939605\","
+ "\"table\":{\"cols\":[{\"id\":\"col1\",\"label\":\"column1\","
+ "\"type\":\"number\",\"pattern\":\"\"},"
+ "{\"id\":\"col2\",\"label\":\"column2\",\"type\":\"boolean\",\"pattern\":\"\"},"
+ "{\"id\":\"col3\",\"label\":\"column3\",\"type\":\"string\",\"pattern\":\"\"}],"
+ "\"rows\":[{\"c\":[{\"v\":7.0},{\"v\":false},{\"v\":\"Why?\"}]}]}}",
json.render(dataSourceRequest, dataTable));
JsonRenderer jsonp = new JsonpRenderer();
// With out:jsonp;
dataSourceRequest = new DataSourceRequest(
new DataSourceParameters("out:jsonp"));
assertEquals(
"google.visualization.Query.setResponse("
+ "{\"version\":\"0.6\",\"status\":\"ok\",\"sig\":\"1548939605\","
+ "\"table\":{\"cols\":[{\"id\":\"col1\",\"label\":\"column1\","
+ "\"type\":\"number\",\"pattern\":\"\"},"
+ "{\"id\":\"col2\",\"label\":\"column2\",\"type\":\"boolean\",\"pattern\":\"\"},"
+ "{\"id\":\"col3\",\"label\":\"column3\",\"type\":\"string\",\"pattern\":\"\"}],"
+ "\"rows\":[{\"c\":[{\"v\":7.0},{\"v\":false},{\"v\":\"Why?\"}]}]}});",
jsonp.render(dataSourceRequest, dataTable));
TsvRenderer tsv = new TsvRenderer();
// Now with out:csv;
dataSourceRequest = new DataSourceRequest(
new DataSourceParameters("out:tsv-excel"));
assertEquals(TsvRenderer.BYTE_ORDER_MARK +
"\"column1\"\t\"column2\"\t\"column3\"\n7\tfalse\t\"Why?\"\n",
tsv.render(dataSourceRequest, dataTable));
CsvRenderer csv = new CsvRenderer();
// Now with out:tsv-excel;
dataSourceRequest = new DataSourceRequest(
new DataSourceParameters("out:csv;reqId:7"));
assertEquals(
"\"column1\",\"column2\",\"column3\"\n7,false,\"Why?\"\n",
csv.render(dataSourceRequest, dataTable));
}
public void testVerifyAccessApprovedTest() throws DataSourceException {
HttpServletRequest req = createNiceMock(HttpServletRequest.class);
// Verify that json from same domain is approved.
setupHttpRequestMock(req, true, "out:json");
DataSourceService helper = new DataSourceService();
helper.verifyAccessApproved(new DataSourceRequest(req));
verify(req);
// Verify that json from cross domain is denied.
setupHttpRequestMock(req, false, "out:json");
try {
helper.verifyAccessApproved(new DataSourceRequest(req));
fail();
}
catch(DataSourceException e) {
// This is the expected behavior.
}
verify(req);
// Verify that csv from cross domain is approved.
setupHttpRequestMock(req, false, "out:csv");
helper.verifyAccessApproved(new DataSourceRequest(req));
verify(req);
// Verify that html from cross domain is approved.
setupHttpRequestMock(req, false, "out:html");
helper.verifyAccessApproved(new DataSourceRequest(req));
verify(req);
// Verify that tsv-excel from cross domain is approved.
setupHttpRequestMock(req, false, "out:tsv-excel");
helper.verifyAccessApproved(new DataSourceRequest(req));
verify(req);
}
public void testParseQueryErrors() {
DataTable dataTable = new DataTable();
dataTable.addColumn(new ColumnDescription("A", ValueType.TEXT, "column1"));
dataTable.addColumn(new ColumnDescription("B", ValueType.BOOLEAN, "column2"));
dataTable.addColumn(new ColumnDescription("C", ValueType.NUMBER, "column3"));
// Wrong column id
checkQueryError("select D", dataTable, "Column [D] does not exist in table.");
checkQueryError("where F > 1", dataTable, "Column [F] does not exist in table.");
// aggregation sum and average only on numeric columns
checkQueryError("select avg(A) group by B", dataTable,
"'Average' and 'sum' aggreagation functions can be applied only on numeric values.");
checkQueryError("select sum(A) group by B", dataTable,
"'Average' and 'sum' aggreagation functions can be applied only on numeric values.");
checkQueryError("select sum(B) group by A", dataTable,
"'Average' and 'sum' aggreagation functions can be applied only on numeric values.");
// parse errors
checkQueryError("select (", dataTable, "Query parse error: ", true);
checkQueryError("group by avg(A)", dataTable,
"Column [AVG(`A`)] cannot be in GROUP BY because it has an aggregation.");
checkQueryError("pivot avg(A)", dataTable,
"Column [AVG(`A`)] cannot be in PIVOT because it has an aggregation.");
checkQueryError("where avg(A) > 1", dataTable,
"Column [AVG(`A`)] cannot appear in WHERE because it has an aggregation.");
checkQueryError("select A, sum(A)", dataTable,
"Column [A] cannot be selected both with and without aggregation in SELECT.");
checkQueryError("select sum(C) group by C", dataTable,
"Column [C] which is aggregated in SELECT, cannot appear in GROUP BY.");
checkQueryError("select A group by C", dataTable,
"Cannot use GROUP BY when no aggregations are defined in SELECT.");
checkQueryError("select A pivot C", dataTable,
"Cannot use PIVOT when no aggregations are defined in SELECT.");
checkQueryError("select min(B) pivot B", dataTable,
"Column [B] which is aggregated in SELECT, cannot appear in PIVOT.");
checkQueryError("select A format B 'yes:no'", dataTable,
"Column [`B`] which is referenced in FORMAT, is not part of SELECT clause.");
checkQueryError("select A label B 'COL'", dataTable,
"Column [`B`] which is referenced in LABEL, is not part of SELECT clause.");
checkQueryError("select A,count(B)", dataTable,
"Column [A] should be added to GROUP BY, removed from SELECT, or aggregated in SELECT.");
checkQueryError("select B order by min(A)", dataTable,
"Aggregation [MIN(`A`)] found in ORDER BY but was not found in SELECT");
checkQueryError("select min(A) pivot B order by min(A)", dataTable,
"Column [A] cannot be aggregated in ORDER BY when PIVOT is used.");
checkQueryError("select min(A) order by B", dataTable,
"Column [`B`] which appears in ORDER BY, must be in SELECT as well, " +
"because SELECT contains aggregated columns.");
checkQueryError("select min(A) group by B pivot B", dataTable,
"Column [B] cannot appear both in GROUP BY and in PIVOT.");
checkQueryError("offset -1", dataTable,
"Invalid value for row offset: -1");
checkQueryError("select avg(C),avg(C)", dataTable,
"Column [avg(C)] cannot appear more than once in SELECT.");
}
private void checkQueryError(String query, DataTable dataTable, String expectedMessage) {
checkQueryError(query, dataTable, expectedMessage, false);
}
private void checkQueryError(String query, DataTable dataTable, String expectedMessage,
boolean startsWith) {
try {
DataSourceService helper = new DataSourceService();
helper.applyQuery(
DataSourceRequest.parseQuery(query), dataTable, null);
fail("Exception should be thrown for query " + query);
}
catch(DataSourceException dse) {
if(startsWith) {
assertNotNull(dse.getMessageToUser());
assertTrue(dse.getMessageToUser().startsWith(expectedMessage));
}
else {
assertEquals(expectedMessage, dse.getMessageToUser());
}
}
}
private void setupHttpRequestMock(HttpServletRequest req, boolean hasHeader, String tqx) {
reset(req);
expect(req.getHeader(DataSourceRequest.SAME_ORIGIN_HEADER)).andReturn(hasHeader ? "a" : null);
expect(req.getParameter(DataSourceRequest.DATASOURCE_REQUEST_PARAMETER)).andReturn(tqx);
replay(req);
}
}