GvizHelper.java
/**
* GvizHelper.java This file is part of WattDepot.
* <p/>
* Copyright (C) 2013 Yongwen Xu
* <p/>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* <p/>
* This program 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 General Public License for more details.
* <p/>
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.wattdepot.common.util;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import javax.xml.datatype.DatatypeConfigurationException;
import javax.xml.datatype.XMLGregorianCalendar;
import org.restlet.resource.ServerResource;
import org.wattdepot.common.domainmodel.InterpolatedValue;
import org.wattdepot.common.domainmodel.InterpolatedValueList;
import org.wattdepot.common.domainmodel.Measurement;
import org.wattdepot.common.domainmodel.MeasurementList;
import org.wattdepot.common.domainmodel.MeasurementType;
import com.google.visualization.datasource.base.DataSourceException;
import com.google.visualization.datasource.base.ReasonType;
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.DateTimeValue;
import com.google.visualization.datasource.datatable.value.ValueType;
import com.google.visualization.datasource.render.JsonRenderer;
import org.wattdepot.common.domainmodel.XYInterpolatedValue;
import org.wattdepot.common.domainmodel.XYInterpolatedValueList;
/**
* GvizHelper - Utility class that handles Google Visualization using the Google Visualization
* Datasource library.
*
* * @see <a
* href="http://code.google.com/apis/chart/interactive/docs/dev/implementing_data_source.html">Google
* Visualization Datasource API</a>
*
* @author Yongwen Xu
*
*/
public class GvizHelper {
/** Conversion factor for milliseconds per minute. */
private static final long MILLISECONDS_PER_MINUTE = 60L * 1000;
/**
* @param server
* the restlet ServerResource
* @param type
* type of the gviz query string, such as tqx, or tq
* @return the value of the gviz query string from the request
*/
public static String getGvizQueryString(ServerResource server, String type) {
String qs = server.getRequest().getResourceRef().getQueryAsForm().getFirstValue(type);
try {
if (qs != null) {
qs = URLDecoder.decode(qs, "UTF-8");
}
}
catch (UnsupportedEncodingException e) {
qs = null;
}
return qs;
}
/**
* @param resource
* server resource object
* @param tqxString
* gviz tqx query string, i.e., request id
* @param tqString
* gviz tq query string, selectable fields
* @return gviz response
*/
public static String getGvizResponse(Object resource, String tqxString, String tqString) {
DataTable table = null;
try {
if (resource instanceof InterpolatedValue) {
table = getDataTable((InterpolatedValue) resource);
}
if (resource instanceof MeasurementList) {
table = getDataTable((MeasurementList) resource);
}
if (resource instanceof InterpolatedValueList) {
table = getDataTable((InterpolatedValueList) resource);
}
if (resource instanceof XYInterpolatedValueList) {
table = getDataTable((XYInterpolatedValueList) resource);
}
}
catch (DataSourceException e) {
return getGvizDataErrorResponse(e);
}
return getGvizResponseFromDataTable(table, tqxString/*, tqString*/);
}
/**
* @param mValue
* mesured value
* @return gviz DataTable
* @throws DataSourceException
* data source exception
*/
private static DataTable getDataTable(InterpolatedValue mValue) throws DataSourceException {
MeasurementType mType = mValue.getMeasurementType();
DataTable data = new DataTable();
try {
data.addColumn(
new ColumnDescription("Date", ValueType.DATETIME, "Date & Time"));
data.addColumn(
new ColumnDescription(mType.getName(), ValueType.NUMBER, mType.toString()));
TableRow row = new TableRow();
XMLGregorianCalendar xgcal = DateConvert.convertDate(mValue.getStart());
row.addCell(new DateTimeValue(convertTimestamp(xgcal)));
row.addCell(mValue.getValue());
data.addRow(row);
}
catch (TypeMismatchException | DatatypeConfigurationException e) {
throw new DataSourceException(ReasonType.INTERNAL_ERROR, "Problem adding data to table"); // NOPMD
}
return data;
}
/**
* @param mList
* measurement list
* @return gviz data table
* @throws DataSourceException
* data source exception
*/
private static DataTable getDataTable(MeasurementList mList) throws DataSourceException {
DataTable data = new DataTable();
// Sets up the columns requested by any SELECT in the datasource query
ArrayList<Measurement> measurements = mList.getMeasurements();
if (!measurements.isEmpty()) {
String type = measurements.get(0).getMeasurementType();
try {
data.addColumn(
new ColumnDescription("Timestamp", ValueType.DATETIME, "Date & Time"));
data.addColumn(
new ColumnDescription(type, ValueType.NUMBER, type));
for (Measurement measurement : measurements) {
TableRow row = new TableRow();
XMLGregorianCalendar xgcal = DateConvert.convertDate(measurement.getDate());
row.addCell(new DateTimeValue(convertTimestamp(xgcal)));
row.addCell(measurement.getValue());
data.addRow(row);
}
}
catch (NumberFormatException e) {
// String value in database couldn't be converted to a number.
throw new DataSourceException(ReasonType.INTERNAL_ERROR, "Found bad number in database"); // NOPMD
}
catch (TypeMismatchException | DatatypeConfigurationException e) {
throw new DataSourceException(ReasonType.INTERNAL_ERROR, "Problem adding data to table"); // NOPMD
}
}
return data;
}
/**
* @param mList
* measured value list
* @return gviz data table
* @throws DataSourceException
* data source exception
*/
private static DataTable getDataTable(InterpolatedValueList mList) throws DataSourceException {
DataTable data = new DataTable();
// Sets up the columns requested by any SELECT in the datasource query
ArrayList<InterpolatedValue> values = mList.getInterpolatedValues();
if (!values.isEmpty()) {
String type = values.get(0).getMeasurementType().getUnits();
try {
data.addColumn(
new ColumnDescription("Timestamp", ValueType.DATETIME, "Date & Time"));
data.addColumn(
new ColumnDescription(type, ValueType.NUMBER, type));
for (InterpolatedValue value : values) {
TableRow row = new TableRow();
XMLGregorianCalendar xgcal = DateConvert.convertDate(value.getStart());
row.addCell(new DateTimeValue(convertTimestamp(xgcal)));
if (value.getValue() != null) {
row.addCell(value.getValue());
}
data.addRow(row);
}
}
catch (NumberFormatException e) {
// String value in database couldn't be converted to a number.
throw new DataSourceException(ReasonType.INTERNAL_ERROR, "Found bad number in database"); // NOPMD
}
catch (TypeMismatchException | DatatypeConfigurationException e) {
throw new DataSourceException(ReasonType.INTERNAL_ERROR, "Problem adding data to table"); // NOPMD
}
}
return data;
}
/**
* @param list The XYInterpolatedValueList.
* @return The gviz data table.
* @throws DataSourceException if there is a data source problem.
*/
protected static DataTable getDataTable(XYInterpolatedValueList list) throws DataSourceException {
DataTable data = new DataTable();
ArrayList<XYInterpolatedValue> values = list.getValues();
if (!values.isEmpty()) {
String xType = values.get(0).getXMeasurementType().getUnits();
String yType = values.get(0).getYMeasurementType().getUnits();
try {
data.addColumn(new ColumnDescription(xType, ValueType.NUMBER, xType));
data.addColumn(new ColumnDescription(yType, ValueType.NUMBER, yType));
for (XYInterpolatedValue value : values) {
TableRow row = new TableRow();
if (value.getXValue() != null && value.getYValue() != null) {
row.addCell(value.getXValue());
row.addCell(value.getYValue());
}
data.addRow(row);
}
}
catch (NumberFormatException e) {
// String value in database couldn't be converted to a number.
throw new DataSourceException(ReasonType.INTERNAL_ERROR, "Found bad number in database"); // NOPMD
}
catch (TypeMismatchException e) {
throw new DataSourceException(ReasonType.INTERNAL_ERROR, "Problem adding data to table"); // NOPMD
}
}
return data;
}
/**
* @param e
* DataSourceException
* @return the error message
*/
protected static String getGvizDataErrorResponse(DataSourceException e) {
return "({status:'error',errors:[{reason:'"
+ e.getReasonType().toString() + "',message:'" + e.getMessageToUser() + "'}]});";
}
/**
* @param table
* gviz data table
* @param tqxString
* gviz query string
* @return the gviz json response
*/
protected static String getGvizResponseFromDataTable(DataTable table, String tqxString/*, String tqString*/) {
String response = "google.visualization.Query.setResponse";
String reqId = null;
if (tqxString != null) {
String[] tqxArray = tqxString.split(";");
for (String s : tqxArray) {
if (s.contains("reqId")) {
reqId = s.substring(s.indexOf(":") + 1, s.length());
}
}
}
String tableString = JsonRenderer.renderDataTable(table, true, true, true).toString();
response += "({status:'ok',";
if (reqId != null) {
response += "reqId:'" + reqId + "',";
}
response += "table:" + tableString + "});";
return response;
}
/**
* Takes an XMLGregorianCalendar (the native WattDepot timestamp format) and returns a
* com.ibm.icu.util.GregorianCalendar (<b>note:</b>
* this is <b>not</b> the same as a java.util.GregorianCalendar!) with the time zone set to GMT,
* but with the timestamp adjusted by the provided offset.
*
* For example, if the input value is 10 AM Hawaii Standard Time (HST is -10 hours from UTC), and
* the offset is -600 minutes then the output will be 10 AM UTC, effectively subtracting 10 hours
* from the timestamp (in addition to the conversion between types).
*
* This rigamarole is needed because the Google Visualization data source library only accepts
* timestamps in UTC, and the Annonated Timeline visualization displays the UTC values. Without
* this conversion, any graphs of meter data recorded in the Hawaii time zone (for instance) would
* display the data 10 hours later in the graph, which is not acceptable. The timezoneOffset is
* provided if in the future we want the capability to display data normalized to an arbitrary
* time zone, rather than just the one the data was collected in.
*
* @param timestamp the input XMLGregorianCalendar timestamp
* @return a com.ibm.icu.util.GregorianCalendar suitable for use by the Google visualization data
* source library, and normalized by timeZoneOffset but with timeZone field set to GMT.
*/
private static com.ibm.icu.util.GregorianCalendar convertTimestamp(XMLGregorianCalendar timestamp) {
// Calendar conversion hell. GViz library uses com.ibm.icu.util.GregorianCalendar, which is
// different from java.util.GregorianCalendar. So we go from our native format
// XMLGregorianCalendar -> GregorianCalendar, extract milliseconds since epoch, feed that
// to the IBM GregorianCalendar.
int timeZoneOffset = timestamp.getTimezone();
com.ibm.icu.util.GregorianCalendar gvizCal = new com.ibm.icu.util.GregorianCalendar();
// Added bonus, DateTimeValue only accepts GregorianCalendars if the timezone is GMT.
gvizCal.setTimeZone(com.ibm.icu.util.TimeZone.getTimeZone("GMT"));
java.util.GregorianCalendar standardCal = timestamp.toGregorianCalendar();
// Add the timeZoneOffset to the epoch time after converting to milliseconds
gvizCal.setTimeInMillis(standardCal.getTimeInMillis()
+ timeZoneOffset * MILLISECONDS_PER_MINUTE);
return gvizCal;
}
}