// 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.render;
import com.google.common.collect.Lists;
import com.google.visualization.datasource.base.DataSourceParameters;
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.base.Warning;
import com.google.visualization.datasource.datatable.ColumnDescription;
import com.google.visualization.datasource.datatable.DataTable;
import com.google.visualization.datasource.datatable.TableCell;
import com.google.visualization.datasource.datatable.TableRow;
import com.google.visualization.datasource.datatable.value.BooleanValue;
import com.google.visualization.datasource.datatable.value.DateTimeValue;
import com.google.visualization.datasource.datatable.value.DateValue;
import com.google.visualization.datasource.datatable.value.NumberValue;
import com.google.visualization.datasource.datatable.value.TimeOfDayValue;
import com.google.visualization.datasource.datatable.value.Value;
import com.google.visualization.datasource.datatable.value.ValueType;
import com.ibm.icu.util.GregorianCalendar;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrBuilder;
import java.util.List;
import java.util.Map;
/**
* Takes a data table and returns a json string.
*
* @author Nimrod T.
*/
public class JsonRenderer {
/**
* Private constructor.
*/
private JsonRenderer() {}
/**
* Returns a String-form 32-bit hash of this table's json.
* Note: the signature ignores formatting.
*
* @param data The data table.
*
* @return a String-form 64-bit hash of this table.
*/
public static String getSignature(DataTable data) {
String tableAsString = renderDataTable(data, true, false).toString();
// Casting to long to avoid bug with abs(Integer.MIN_VALUE) being negative.
long longHashCode = tableAsString.hashCode();
return String.valueOf(Math.abs(longHashCode));
}
/**
* Returns the part of the json string that describes a single warning or error.
*
* @param reasonType The reason for the warning or error.
* @param description The detailed description for this warning or error.
*
* @return The part of the json string.
*/
private static String getFaultString(ReasonType reasonType, String description) {
List<String> objectParts = Lists.newArrayList();
if (reasonType != null) {
objectParts.add("reason:'" + reasonType.lowerCaseString() + "'");
objectParts.add("message:'" + EscapeUtil.jsonEscape(
reasonType.getMessageForReasonType(null)) + "'");
}
if (description != null) {
objectParts.add("detailed_message:'" + EscapeUtil.jsonEscape(description)
+ "'");
}
return new StrBuilder("{").appendWithSeparators(objectParts, ",").append("}").toString();
}
/**
* Returns the json response for the given data table.
*
* @param dsParams The datasource parameters.
* @param responseStatus The response status.
* @param data The data table.
*
* @return The json response for the given data table and parameters.
*/
public static CharSequence renderJsonResponse(
DataSourceParameters dsParams,
ResponseStatus responseStatus,
DataTable data,
boolean isJsonp) {
StrBuilder sb = new StrBuilder();
if (isJsonp) {
sb.append(dsParams.getResponseHandler()).append("(");
}
sb.append("{version:'0.6'");
// If no reqId found in the request, do not return reqId in the response.
String requestId = dsParams.getRequestId();
if (requestId != null) {
sb.append(",reqId:'").append(EscapeUtil.jsonEscape(requestId)).append("'");
}
// Check signature.
String previousSignature = dsParams.getSignature();
if (responseStatus == null) {
if (!StringUtils.isEmpty(previousSignature) && (data != null)
&& (JsonRenderer.getSignature(data).equals(previousSignature))) {
responseStatus = new ResponseStatus(StatusType.ERROR, ReasonType.NOT_MODIFIED, null);
} else {
responseStatus = new ResponseStatus(StatusType.OK, null, null);
}
}
StatusType statusType = responseStatus.getStatusType();
sb.append(",status:'").append(statusType.lowerCaseString()).append("'");
// There are reason and messages if the status is WARNING/ERROR.
if (statusType != StatusType.OK) {
// Status is warning or error.
if (statusType == StatusType.WARNING) {
List<Warning> warnings = data.getWarnings();
List<String> warningJsonStrings = Lists.newArrayList();
if (warnings != null) {
for (Warning warning : warnings) {
warningJsonStrings.add(getFaultString(warning.getReasonType(), warning.getMessage()));
}
}
sb.append(",warnings:[").appendWithSeparators(warningJsonStrings, ",").append("]");
} else { // Status is error.
sb.append(",errors:[");
sb.append(getFaultString(responseStatus.getReasonType(), responseStatus.getDescription()));
sb.append("]");
}
}
if ((statusType != StatusType.ERROR) && (data != null)) {
// MessageType OK or WARNING,
// so need to attach a data table (and a signature).
sb.append(",sig:'").append(JsonRenderer.getSignature(data)).append("'");
sb.append(",table:").append(JsonRenderer.renderDataTable(data, true, true));
}
sb.append("}");
if (isJsonp) {
sb.append(");");
}
return sb.toString();
}
/**
* Generates a JSON representation of the data table object.
*
* @param includeValues False if the json should contain just meta-data and column descriptions
* but without the data rows.
* @param includeFormatting False if formatting information should be omitted from the
* generated json.
*
* @return The char sequence with the Json string.
*/
public static CharSequence renderDataTable(DataTable dataTable, boolean includeValues,
boolean includeFormatting) {
if (dataTable.getColumnDescriptions().isEmpty()) {
return "";
}
List<ColumnDescription> columnDescriptions = dataTable.getColumnDescriptions();
StringBuilder sb = new StringBuilder();
sb.append("{");
sb.append("cols:["); // column descriptions.
ColumnDescription col;
for (int colId = 0; colId < columnDescriptions.size(); colId++) {
col = columnDescriptions.get(colId);
appendColumnDescriptionJson(col, sb);
if (colId != (columnDescriptions.size() - 1)) {
sb.append(",");
}
}
sb.append("]"); // columns.
if (includeValues) {
sb.append(",rows:[");
List<TableCell> cells;
TableCell cell;
ColumnDescription columnDescription;
List<TableRow> rows = dataTable.getRows();
for (int rowId = 0; rowId < rows.size(); rowId++) {
TableRow tableRow = rows.get(rowId);
cells = tableRow.getCells();
sb.append("{c:[");
for (int cellId = 0; cellId < cells.size(); cellId++) {
cell = cells.get(cellId);
if (cellId < (cells.size() - 1)) {
appendCellJson(cell, sb, includeFormatting, false);
sb.append(",");
} else {
// Last column in the row.
appendCellJson(cell, sb, includeFormatting, true);
}
}
sb.append("]");
// Row properties.
String customPropertiesString = getPropertiesMapString(tableRow.getCustomProperties());
if (customPropertiesString != null) {
sb.append(",p:").append(customPropertiesString);
}
sb.append("}"); // cells.
if ((rows.size() - 1) > rowId) {
sb.append(",");
}
}
sb.append("]"); // rows.
}
// Table properties.
String customPropertiesString = getPropertiesMapString(dataTable.getCustomProperties());
if (customPropertiesString != null) {
sb.append(",p:").append(customPropertiesString);
}
sb.append("}"); // table.
return sb;
}
/**
* Appends a Json representing a cell to the string buffer.
*
* @param cell The cell to write Json for.
* @param sb The string buffer to append to.
* @param includeFormatting Flase if formatting information should be omitted from the json.
* @param isLastColumn Is this the last column in the row.
*
* @return The input string builder.
*/
public static StringBuilder appendCellJson(TableCell cell,
StringBuilder sb, boolean includeFormatting, boolean isLastColumn) {
Value value = cell.getValue();
ValueType type = cell.getType();
StringBuilder valueJson = new StringBuilder();
GregorianCalendar calendar;
String escapedFormattedString = "";
boolean isJsonNull = false;
// Prepare a Json string representing the current value.
DateValue dateValue;
TimeOfDayValue timeOfDayValue;
if ((value == null) || (value.isNull())) {
valueJson.append("null");
isJsonNull = true;
} else {
switch (type) {
case BOOLEAN:
valueJson.append(((BooleanValue) value).getValue());
break;
case DATE:
valueJson.append("new Date(");
dateValue = (DateValue) value;
valueJson.append(dateValue.getYear()).append(",");
valueJson.append(dateValue.getMonth()).append(",");
valueJson.append(dateValue.getDayOfMonth());
valueJson.append(")");
break;
case NUMBER:
valueJson.append(((NumberValue) value).getValue());
break;
case TEXT:
valueJson.append("'");
valueJson.append(EscapeUtil.jsonEscape(value.toString()));
valueJson.append("'");
break;
case TIMEOFDAY:
valueJson.append("[");
timeOfDayValue = (TimeOfDayValue) value;
valueJson.append(timeOfDayValue.getHours()).append(",");
valueJson.append(timeOfDayValue.getMinutes()).append(",");
valueJson.append(timeOfDayValue.getSeconds()).append(",");
valueJson.append(timeOfDayValue.getMilliseconds());
valueJson.append("]");
break;
case DATETIME:
calendar = ((DateTimeValue) value).getCalendar();
valueJson.append("new Date(");
valueJson.append(calendar.get(GregorianCalendar.YEAR)).append(",");
valueJson.append(calendar.get(GregorianCalendar.MONTH)).append(",");
valueJson.append(calendar.get(GregorianCalendar.DAY_OF_MONTH));
valueJson.append(",");
valueJson.append(calendar.get(GregorianCalendar.HOUR_OF_DAY));
valueJson.append(",");
valueJson.append(calendar.get(GregorianCalendar.MINUTE)).append(",");
valueJson.append(calendar.get(GregorianCalendar.SECOND));
valueJson.append(")");
break;
default:
throw new IllegalArgumentException("Illegal value Type " + type);
}
}
// Prepare an escaped string representing the current formatted value.
String formattedValue = cell.getFormattedValue();
if ((value != null) && !value.isNull() && (formattedValue != null)) {
escapedFormattedString = EscapeUtil.jsonEscape(formattedValue);
// Check for a value of type TEXT if the formatted value equals
// its ordinary toString.
if ((type == ValueType.TEXT) && value.toString().equals(formattedValue)) {
escapedFormattedString = "";
}
}
// Add a Json for this cell. And,
// 1) If the formatted value is empty drop it.
// 2) If the value is null, and it is not the last column in the row drop the entire Json.
if ((isLastColumn) || (!isJsonNull)) {
sb.append("{");
// Value
sb.append("v:").append(valueJson);
// Formatted value
if ((includeFormatting) && (!escapedFormattedString.equals(""))) {
sb.append(",f:'").append(escapedFormattedString).append("'");
}
String customPropertiesString = getPropertiesMapString(cell.getCustomProperties());
if (customPropertiesString != null) {
sb.append(",p:").append(customPropertiesString);
}
sb.append("}");
}
return sb;
}
/**
* Appends a Json representing a column description to the string buffer.
*
* @param col The column description to write Json for.
* @param sb The string builder to append to.
*
* @return The input string builder.
*/
public static StringBuilder appendColumnDescriptionJson(
ColumnDescription col, StringBuilder sb) {
sb.append("{");
sb.append("id:'").append(EscapeUtil.jsonEscape(col.getId())).append("',");
sb.append("label:'").append(EscapeUtil.jsonEscape(col.getLabel())).append("',");
sb.append("type:'").append(col.getType().getTypeCodeLowerCase()).append("',");
sb.append("pattern:'").append(EscapeUtil.jsonEscape(col.getPattern())).append("'");
String customPropertiesString = getPropertiesMapString(col.getCustomProperties());
if (customPropertiesString != null) {
sb.append(",p:").append(customPropertiesString);
}
sb.append("}");
return sb;
}
/**
* Makes a string from a properties map.
*
* @param propertiesMap The properties map.
*
* @return A json string.
*/
private static String getPropertiesMapString(Map<String, String> propertiesMap) {
String customPropertiesString = null;
if ((propertiesMap != null) && (!propertiesMap.isEmpty())) {
List<String> customPropertiesStrings = Lists.newArrayList();
for (Map.Entry<String, String> entry : propertiesMap.entrySet()) {
customPropertiesStrings.add("'"
+ EscapeUtil.jsonEscape(entry.getKey()) + "':'"
+ EscapeUtil.jsonEscape(entry.getValue()) + "'");
}
customPropertiesString = new StrBuilder("{")
.appendWithSeparators(customPropertiesStrings, ",").append("}").toString();
}
return customPropertiesString;
}
}