/*
* JBoss, Home of Professional Open Source.
* See the COPYRIGHT.txt file distributed with this work for information
* regarding copyright ownership. Some portions may be licensed
* to Red Hat, Inc. under one or more contributor license agreements.
*
* This library 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 library 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*/
package org.teiid.resource.adapter.google.dataprotocol;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.resource.adapter.google.auth.AuthHeaderFactory;
import org.teiid.translator.google.api.SpreadsheetAuthException;
import org.teiid.translator.google.api.SpreadsheetOperationException;
import org.teiid.translator.google.api.metadata.Column;
import org.teiid.translator.google.api.metadata.SpreadsheetColumnType;
import org.teiid.translator.google.api.metadata.SpreadsheetInfo;
import org.teiid.translator.google.api.result.PartialResultExecutor;
import org.teiid.translator.google.api.result.RowsResult;
import org.teiid.translator.google.api.result.SheetRow;
/**
*
* This class is used to make requests to Google Visualization Data Protocol. The most important
* method is executeQuery.
*
* @author fnguyen
*
*/
public class GoogleDataProtocolAPI {
private AuthHeaderFactory headerFactory = null;
public static String ENCODING = "UTF-8"; //$NON-NLS-1$
private GoogleJSONParser parser = new GoogleJSONParser();
public AuthHeaderFactory getHeaderFactory() {
return headerFactory;
}
public void setHeaderFactory(AuthHeaderFactory headerFactory) {
this.headerFactory = headerFactory;
}
/**
* Most important method that will issue query [1] to specific worksheet. The columns in the query
* should be identified by their real alphabetic name (A, B, C...).
*
* There is one important restriction to query. It should not contain offset and limit clauses.
* To achieve functionality of offset and limit please use corresponding parameters in this method.
*
*
* [1] https://developers.google.com/chart/interactive/docs/querylanguage
*
* @param query The query defined in [1]
* @param batchSize How big portions of data should be returned by one roundtrip to Google.
* @return Iterable RowsResult that will actually perform the roundtrips to Google for data
*/
public RowsResult executeQuery(SpreadsheetInfo info, String worksheetTitle,
String query, int batchSize, Integer offset, Integer limit) {
String key = info.getSpreadsheetKey();
RowsResult result = new RowsResult(new DataProtocolQueryStrategy(key,worksheetTitle,query), batchSize);
if (offset!= null)
result.setOffset(offset);
if (limit != null)
result.setLimit(limit);
return result;
}
public List<Column> getMetadata(String key, String worksheetTitle) {
DataProtocolQueryStrategy dpqs = new DataProtocolQueryStrategy(key,worksheetTitle,"SELECT *"); //$NON-NLS-1$
dpqs.getResultsBatch(0, 1);
return dpqs.getMetadata();
}
/**
* Logic to query portion of data from Google Visualization Data Protocol. We do not use any special library just simple
* Http request. Google sends response back in CSV that we parse afterwards.
*
* @author fnguyen
*
*/
public class DataProtocolQueryStrategy implements PartialResultExecutor {
private String spreadsheetKey;
private String worksheetName;
private String urlEncodedQuery;
private List<Column> metadata;
public DataProtocolQueryStrategy(String key, String worksheetKey,
String query) {
super();
this.spreadsheetKey = key;
this.worksheetName = worksheetKey;
try {
this.urlEncodedQuery = URLEncoder.encode(query, ENCODING);
} catch (UnsupportedEncodingException e) {
throw new SpreadsheetOperationException(e);
}
}
public List<Column> getMetadata() {
return metadata;
}
public List<SheetRow> getResultsBatch(int startIndex, int amount) {
String boundariedQuery =null;
String worksheet = null;
try {
boundariedQuery = getQueryWithBoundaries(amount, Math.max(0,(startIndex)));
worksheet = URLEncoder.encode(worksheetName, ENCODING);
} catch (UnsupportedEncodingException e) {
throw new SpreadsheetOperationException(e);
}
HttpGet get = new HttpGet("https://spreadsheets.google.com/tq?key="+spreadsheetKey+"&sheet="+worksheet+"&tqx=responseHandler:x;out:json&tq="+boundariedQuery); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
get.setHeader("GData-Version", "3.0"); //$NON-NLS-1$ //$NON-NLS-2$
get.setHeader("Authorization", headerFactory.getAuthHeader()); //$NON-NLS-1$
try {
DefaultHttpClient client = new DefaultHttpClient();
try {
return executeAndParse(client, get);
} catch (SpreadsheetAuthException e) {
// relogin
headerFactory.login();
get.setHeader("Authorization", headerFactory.getAuthHeader()); //$NON-NLS-1$
return executeAndParse(client, get);
}
} catch (IOException e) {
throw new SpreadsheetOperationException("Error retrieving batch from Gogole Visualization Data protocol", e);
}
}
private List<SheetRow> executeAndParse(HttpClient client, HttpGet get) throws IOException {
HttpResponse response = client.execute(get);
if (response.getStatusLine().getStatusCode() == 200)
{
Calendar cal = null;
Reader reader = null;
try {
reader = new InputStreamReader(response.getEntity().getContent(), Charset.forName(ENCODING));
Map<?, ?> jsonResponse = (Map<?, ?>)parser.parseObject(reader, true);
String status = (String)jsonResponse.get("status"); //$NON-NLS-1$
if ("error".equals(status)) { //$NON-NLS-1$
//TODO: better formatting
List<Map<?, ?>> errors = (List<Map<?, ?>>) jsonResponse.get("errors"); //$NON-NLS-1$
List<String> reasons = new ArrayList<String>();
for (Map<?, ?> map : errors) {
String reason = (String)map.get("reason"); //$NON-NLS-1$
if ("user_not_authenticated".equals(reason)) { //$NON-NLS-1$
throw new SpreadsheetAuthException("User not authenticated");
}
reasons.add(reason);
}
LogManager.logDetail(LogConstants.CTX_CONNECTOR, "Google request failed", errors); //$NON-NLS-1$
throw new SpreadsheetOperationException(reasons.toString());
}
//TODO: the warning could be sent to the client via the ExecutionContext
Map<?,?> table = (Map<?,?>)jsonResponse.get("table"); //$NON-NLS-1$
List<Map<?, ?>> cols = (List<Map<?, ?>>) table.get("cols"); //$NON-NLS-1$
this.metadata = new ArrayList<Column>(cols.size());
for (Map<?, ?> col : cols) {
Column c = new Column();
c.setAlphaName((String) col.get("id")); //$NON-NLS-1$
String label = (String)col.get("label"); //$NON-NLS-1$
if (label != null && !label.isEmpty()) {
c.setLabel(label);
}
String type = (String)col.get("type"); //$NON-NLS-1$
if (type != null) {
c.setDataType(SpreadsheetColumnType.valueOf(type.toUpperCase()));
}
this.metadata.add(c);
}
List<SheetRow> result = new ArrayList<SheetRow>();
List<Map<?,?>> rows = (List<Map<?,?>>) table.get("rows"); //$NON-NLS-1$
for (Map<?,?> row : rows) {
SheetRow returnRow = new SheetRow();
List<Map<?,?>> vals = (List<Map<?,?>>)row.get("c"); //$NON-NLS-1$
int i = -1;
for (Map<?,?> val : vals) {
i++;
if (val == null) {
returnRow.addColumn(null);
continue;
}
Object object = val.get("v"); //$NON-NLS-1$
if (object != null) {
//special handling for time types
Column c = this.metadata.get(i);
object = convertValue(cal, object, c.getDataType());
}
//TODO: empty string values could be interpreted as null
returnRow.addColumn(object);
}
result.add(returnRow);
}
return result;
} finally {
if (reader != null) {
reader.close();
}
}
} else if (response.getStatusLine().getStatusCode() == 500){
//500 from server may not be a actual error. It can mean that offset is higher then actual result size. Can be solved by calling "count" first but performance penalty
return new ArrayList<SheetRow>();
}
else {
throw new SpreadsheetOperationException("Error when getting batch "+response.getStatusLine().getStatusCode()+":" +response.getStatusLine().getReasonPhrase());
}
}
/**
* Adds limit, offset to the query. This is slightly more complicated becuase limit/offset must appear
* before certain clauses (label, format, options)
* @param amount
* @param offset
* @return
* @throws UnsupportedEncodingException
*/
private String getQueryWithBoundaries(int amount, int offset) throws UnsupportedEncodingException {
String[] keywordsToJump = new String[] {"label","format","options"}; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
int indexToPut = urlEncodedQuery.length();
for (String jumpIt : keywordsToJump){
int index = urlEncodedQuery.indexOf(jumpIt);
if (index != -1) {
indexToPut = index;
break;
}
}
return urlEncodedQuery.substring(0, indexToPut).toString() +URLEncoder.encode(" limit "+amount+" offset "+offset+" ",ENCODING).toString() //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ urlEncodedQuery.substring(indexToPut).toString();
}
}
static Object convertValue(Calendar cal, Object object, SpreadsheetColumnType type) {
switch (type) {
case DATE:
case DATETIME:
if (object instanceof String) {
String stringVal = (String)object;
if (stringVal.startsWith("Date(") && stringVal.endsWith(")")) { //$NON-NLS-1$ //$NON-NLS-2$
String[] parts = stringVal.substring(5, stringVal.length() - 1).split(","); //$NON-NLS-1$
if (cal == null) {
cal = Calendar.getInstance();
}
cal.clear();
if (type == SpreadsheetColumnType.DATETIME) {
cal.set(Integer.valueOf(parts[0]), Integer.valueOf(parts[1]), Integer.valueOf(parts[2]), Integer.valueOf(parts[3]), Integer.valueOf(parts[4]), Integer.valueOf(parts[5]));
object = new Timestamp(cal.getTimeInMillis());
} else {
cal.set(Integer.valueOf(parts[0]), Integer.valueOf(parts[1]), Integer.valueOf(parts[2]));
object = new Date(cal.getTimeInMillis());
}
}
}
break;
case TIMEOFDAY:
if (object instanceof List<?>) {
List<Double> doubleVals = (List<Double>)object;
if (cal == null) {
cal = Calendar.getInstance();
}
cal.clear();
cal.set(Calendar.YEAR, 1970);
cal.set(Calendar.MONTH, Calendar.JANUARY);
cal.set(Calendar.DAY_OF_MONTH, 1);
cal.set(Calendar.MILLISECOND, 0);
cal.set(Calendar.HOUR, doubleVals.get(0).intValue());
cal.set(Calendar.MINUTE, doubleVals.get(1).intValue());
cal.set(Calendar.SECOND, doubleVals.get(2).intValue());
//TODO: it's not proper to convey the millis on a time value
cal.set(Calendar.MILLISECOND, doubleVals.get(3).intValue());
object = new Time(cal.getTimeInMillis());
}
break;
}
return object;
}
}