// 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 org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.StringWriter;
import java.io.Writer;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import com.google.visualization.datasource.DataSourceRequest;
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.ValueFormatter;
import com.google.visualization.datasource.datatable.value.BooleanValue;
import com.google.visualization.datasource.datatable.value.ValueType;
/**
* Takes a data table and returns an html string.
*
* @author Nimrod T.
*/
public class HtmlRenderer extends AbstractRenderer {
private static final Log log = LogFactory.getLog(HtmlRenderer.class);
/**
* Pattern for matching against <a> tags with hrefs.
* Used in sanitizeDetailedMessage.
*/
private static final Pattern DETAILED_MESSAGE_A_TAG_REGEXP = Pattern.compile(
"([^<]*<a(( )*target=\"_blank\")*(( )*target='_blank')*"
+ "(( )*href=\"[^\"]*\")*(( )*href='[^']*')*>[^<]*</a>)+[^<]*");
/**
* Pattern for matching against "javascript:".
* Used in sanitizeDetailedMessage.
*/
private static final Pattern BAD_JAVASCRIPT_REGEXP = Pattern.compile("javascript(( )*):");
@Override
public String getMimetype() {
return "text/html";
}
/**
* Generates an HTML string representation of a data table.
*/
@Override
public CharSequence render(final DataSourceRequest request, final DataTable dataTable) {
// Create an xml document with head and an empty body.
Document document = this.createDocument();
Element bodyElement = this.appendHeadAndBody(document);
// Populate the xml document.
Element tableElement = document.createElement("table");
bodyElement.appendChild(tableElement);
tableElement.setAttribute("border", "1");
tableElement.setAttribute("cellpadding", "2");
tableElement.setAttribute("cellspacing", "0");
// Labels tr element.
List<ColumnDescription> columnDescriptions = dataTable.getColumnDescriptions();
Element trElement = document.createElement("tr");
trElement.setAttribute("style", "font-weight: bold; background-color: #aaa;");
for(ColumnDescription columnDescription : columnDescriptions) {
Element tdElement = document.createElement("td");
tdElement.setTextContent(columnDescription.getLabel());
trElement.appendChild(tdElement);
}
tableElement.appendChild(trElement);
Map<ValueType, ValueFormatter> formatters = ValueFormatter.createDefaultFormatters(request.getUserLocale());
// Table tr elements.
int rowCount = 0;
for(TableRow row : dataTable.getRows()) {
rowCount++;
trElement = document.createElement("tr");
String backgroundColor = (rowCount % 2 != 0) ? "#f0f0f0" : "#ffffff";
trElement.setAttribute("style", "background-color: " + backgroundColor);
List<TableCell> cells = row.getCells();
for(int c = 0; c < cells.size(); c++) {
ValueType valueType = columnDescriptions.get(c).getType();
TableCell cell = cells.get(c);
Element tdElement = document.createElement("td");
if(cell.isNull()) {
tdElement.setTextContent("\u00a0");
}
else {
String cellFormattedText = cell.getFormattedValue();
if(cellFormattedText == null) {
cellFormattedText = formatters.get(cell.getType()).format(cell.getValue());
}
switch(valueType) {
case NUMBER:
tdElement.setAttribute("align", "right");
tdElement.setTextContent(cellFormattedText);
break;
case BOOLEAN:
BooleanValue booleanValue = (BooleanValue) cell.getValue();
tdElement.setAttribute("align", "center");
if(booleanValue.getValue()) {
tdElement.setTextContent("\u2714"); // Check mark.
}
else {
tdElement.setTextContent("\u2717"); // X mark.
}
break;
default:
if(StringUtils.isEmpty(cellFormattedText)) {
tdElement.setTextContent("\u00a0"); // nbsp.
}
else {
tdElement.setTextContent(cellFormattedText);
}
}
}
trElement.appendChild(tdElement);
}
tableElement.appendChild(trElement);
}
bodyElement.appendChild(tableElement);
// Warnings:
for(Warning warning : dataTable.getWarnings()) {
bodyElement.appendChild(document.createElement("br"));
bodyElement.appendChild(document.createElement("br"));
Element messageElement = document.createElement("div");
messageElement.setTextContent(warning.getReasonType().getMessageForReasonType() + ". " +
warning.getMessage());
bodyElement.appendChild(messageElement);
}
return transformDocumentToHtmlString(document);
}
/**
* Transforms a document to a valid html string.
*
* @param document The document to transform
* @return A string representation of a valid html.
*/
private String transformDocumentToHtmlString(Document document) {
// Generate a CharSequence from the xml document.
Transformer transformer = null;
try {
transformer = TransformerFactory.newInstance().newTransformer();
}
catch(TransformerConfigurationException e) {
log.error("Couldn't create a transformer", e);
throw new RuntimeException("Couldn't create a transformer. This should never happen.", e);
}
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "-//W3C//DTD HTML 4.01//EN");
transformer.setOutputProperty(OutputKeys.METHOD, "html");
transformer.setOutputProperty(OutputKeys.VERSION, "4.01");
DOMSource source = new DOMSource(document);
Writer writer = new StringWriter();
StreamResult result = new StreamResult(writer);
try {
transformer.transform(source, result);
}
catch(TransformerException e) {
log.error("Couldn't transform", e);
throw new RuntimeException("Couldn't transform. This should never happen.", e);
}
return writer.toString();
}
/**
* Sanitizes the html in the detailedMessage, to allow only href inside <a> tags.
*
* @param detailedMessage The detailedMessage.
* @return The sanitized detailedMessage.
*/
String sanitizeDetailedMessage(String detailedMessage) {
if(StringUtils.isEmpty(detailedMessage)) {
return "";
}
if(DETAILED_MESSAGE_A_TAG_REGEXP.matcher(detailedMessage).matches()
&& (!BAD_JAVASCRIPT_REGEXP.matcher(detailedMessage).find())) {
// No need to escape.
return detailedMessage;
}
else {
// Need to html escape.
return EscapeUtil.htmlEscape(detailedMessage);
}
}
/**
* Renders a simple html for the given responseStatus.
*
* @param request Data source request
* @param status The response status.
* @return A simple HTML for the given response status.
*/
@Override
public CharSequence error(final DataSourceRequest request, final ResponseStatus status) {
// Get the responseStatus details.
StatusType type = status.getStatusType();
ReasonType reason = status.getReasonType();
String detailedMessage = status.getDescription();
// Create an xml document with head and an empty body.
Document document = createDocument();
Element bodyElement = appendHeadAndBody(document);
// Populate the xml document.
Element oopsElement = document.createElement("h3");
oopsElement.setTextContent("Oops, an error occured.");
bodyElement.appendChild(oopsElement);
if(type != null) {
String text = "Status: " + type.lowerCaseString();
appendSimpleText(document, bodyElement, text);
}
if(reason != null) {
String text = "Reason: " + reason.getMessageForReasonType(null);
appendSimpleText(document, bodyElement, text);
}
if(detailedMessage != null) {
String text = "Description: " + sanitizeDetailedMessage(detailedMessage);
appendSimpleText(document, bodyElement, text);
}
return transformDocumentToHtmlString(document);
}
/**
* Creates a document element.
*
* @return A document element.
*/
private Document createDocument() {
DocumentBuilder documentBuilder;
try {
documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
}
catch(ParserConfigurationException e) {
log.error("Couldn't create a document builder", e);
throw new RuntimeException(
"Couldn't create a document builder. This should never happen.", e);
}
return documentBuilder.newDocument();
}
/**
* Appends <html>, <head>, <title>, and <body> elements to the document.
*
* @param document The containing document.
* @return The <body> element.
*/
private Element appendHeadAndBody(Document document) {
Element htmlElement = document.createElement("html");
document.appendChild(htmlElement);
Element headElement = document.createElement("head");
htmlElement.appendChild(headElement);
Element titleElement = document.createElement("title");
titleElement.setTextContent("Google Visualization");
headElement.appendChild(titleElement);
Element bodyElement = document.createElement("body");
htmlElement.appendChild(bodyElement);
return bodyElement;
}
/**
* Appends a simple text line to the body of the document.
*
* @param document The containing document.
* @param bodyElement The body of the document.
* @param text The text to append.
*/
private void appendSimpleText(Document document, Element bodyElement, String text) {
Element statusElement = document.createElement("div");
statusElement.setTextContent(text);
bodyElement.appendChild(statusElement);
}
}