/*******************************************************************************
* Copyright 2011
* Ubiquitous Knowledge Processing (UKP) Lab
* Technische Universität Darmstadt
*
* 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 org.dkpro.lab.reporting;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.PrintSetup;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.dkpro.lab.storage.StreamReader;
import org.dkpro.lab.storage.StreamWriter;
import au.com.bytecode.opencsv.CSVReader;
import au.com.bytecode.opencsv.CSVWriter;
/**
* Conveniently create a tabular data structure which may be persisted to and read from a CSV file
* or serialized in several other formats.
*
* @param <V>
* cell data type.
*/
public class FlexTable<V>
{
private static final Object PRESENT = new Object();
private LinkedHashMap<String, Object> columns;
private Map<String, Map<String, V>> rows;
private V defaultValue;
private String formatString;
private boolean writeSorted = true;
private boolean compact = true;
private Class<V> dataClass;
private Locale locale = Locale.ENGLISH;
{
columns = new LinkedHashMap<String, Object>();
rows = new LinkedHashMap<String, Map<String, V>>();
}
private FlexTable(Class<V> aDataClass)
{
dataClass = aDataClass;
}
public static <C> FlexTable<C> forClass(Class<C> aClass)
{
return new FlexTable<C>(aClass);
}
/**
* If a cell contains no value, this value is returned when asking for the cell value.
*
* @param aDefaultValue
* the default cell value.
*/
public void setDefaultValue(V aDefaultValue)
{
defaultValue = aDefaultValue;
}
/**
* Set the format use to render cell values. Per default this is set to {@code null} so the
* {@link String#valueOf(Object)} method is used for rendering. If this is set, the method
* {@link String#format(String, Object...)} is used instead.
*
* @param aFormatString
* a format string
*
* @see String#format(String, Object...)
*/
public void setFormatString(String aFormatString)
{
formatString = aFormatString;
}
public Locale getLocale()
{
return locale;
}
/**
* Setting the locale to a non-English locale may break the detection of numeric values in
* writers.
*/
public void setLocale(Locale aLocale)
{
locale = aLocale;
}
/**
* Add a new row. If the row already exists, it is overwritten.
*
* @param aId
* the row ID.
* @param aRow
* the row data.
*/
public void addRow(String aId, Map<String, ? extends V> aRow)
{
LinkedHashMap<String, V> row = new LinkedHashMap<String, V>();
if (aRow != null) {
for (String key : aRow.keySet()) {
columns.put(key, PRESENT);
}
row.putAll(aRow);
}
rows.put(aId, row);
}
/**
* Append new columns to an existing row. If no row with the given ID is present, a new one is
* created.
*
* @param aId
* the row ID.
* @param aRow
* the row data.
*/
public void addToRow(String aId, Map<String, ? extends V> aRow)
{
Map<String, V> row = rows.get(aId);
if (row == null) {
addRow(aId, aRow);
}
else {
for (String key : aRow.keySet()) {
columns.put(key, PRESENT);
}
row.putAll(aRow);
}
}
public Map<String, V> getRow(String aId)
{
return rows.get(aId);
}
public void addColumns(String... aColumnNames)
{
for (String key : aColumnNames) {
columns.put(key, PRESENT);
}
}
public void setColumns(String... aColumnNames)
{
columns.clear();
addColumns(aColumnNames);
}
public String[] getColumnIds()
{
Set<String> keySet = columns.keySet();
return keySet.toArray(new String[keySet.size()]);
}
/**
* Enable/disable compact rendering mode. In compact mode, invariant columns may be rendered as
* a separate section in the output or totally omitted. This is turned on by default. To always
* render all columns, disable this.
*/
public void setCompact(boolean aCompact)
{
compact = aCompact;
}
public boolean isCompact()
{
return compact;
}
/**
* Enable/disable automatic sorting of rows by ID. This is turned on by default. To render rows
* in the order they were added to the table, disable this.
*/
public void setSortRows(boolean aWriteSorted)
{
writeSorted = aWriteSorted;
}
protected String[] getCompactColumnIds(boolean aAllSame)
{
List<String> colIds = new ArrayList<String>();
columns: for (String colId : columns.keySet()) {
String lastValue = null;
for (String rowId : getRowIds()) {
String value = getValueAsString(rowId, colId);
if (lastValue != null && !lastValue.equals(value)) {
// not all the same
if (!aAllSame) {
colIds.add(colId);
}
continue columns;
}
lastValue = value;
}
if (aAllSame) {
colIds.add(colId);
}
}
return colIds.toArray(new String[colIds.size()]);
}
public String[] getRowIds()
{
String[] rowIds = rows.keySet().toArray(new String[rows.size()]);
if (writeSorted) {
Arrays.sort(rowIds);
}
return rowIds;
}
public V getValue(String aRowId, String aColId)
{
Map<String, V> row = rows.get(aRowId);
if (row == null) {
return defaultValue;
}
V value = row.get(aColId);
if (value == null) {
return defaultValue;
}
return value;
}
public String getValueAsString(String aRowId, String aColId)
{
V value = getValue(aRowId, aColId);
if (formatString != null) {
return String.format(locale, formatString, value);
}
else {
return String.valueOf(value);
}
}
public StreamWriter getTextWriter()
{
return new StreamWriter()
{
@Override
public void write(OutputStream aStream)
throws Exception
{
String[] colIds = FlexTable.this.compact ? getCompactColumnIds(false)
: getColumnIds();
// Obtain the width of the columns based on their content and headers
// col 0 is reserved here for the rowId width
int colWidths[] = new int[colIds.length+1];
for (String rowId : getRowIds()) {
colWidths[0] = Math.max(colWidths[0], rowId.length());
}
for (int i = 1; i < colWidths.length; i++) {
colWidths[i] = Math.max(colWidths[i], colIds[i-1].length());
for (String rowId : getRowIds()) {
colWidths[i] = Math.max(colWidths[i], getValueAsString(rowId, colIds[i-1]).length());
}
}
StringBuilder separator = new StringBuilder();
for (int w : colWidths) {
if (separator.length() > 0) {
separator.append("-+-");
}
separator.append(StringUtils.repeat("-", w));
}
separator.insert(0, "+-");
separator.append("-+");
PrintWriter writer = new PrintWriter(new OutputStreamWriter(aStream, "UTF-8"));
// Render header column
writer.println(separator);
writer.print("| ");
writer.print(StringUtils.repeat(" ", colWidths[0]));
for (int i = 0; i < colIds.length; i++) {
writer.print(" | ");
// Remember: colWidth[0] is the rowId width!
writer.print(StringUtils.center(colIds[i], colWidths[i+1]));
}
writer.println(" |");
writer.println(separator);
// Render body
for (String rowId : getRowIds()) {
writer.print("| ");
writer.print(StringUtils.rightPad(rowId, colWidths[0]));
for (int i = 0; i < colIds.length; i++) {
writer.print(" | ");
// Remember: colWidth[0] is the rowId width!
String val = getValueAsString(rowId, colIds[i]);
if (isDouble(val)) {
writer.print(StringUtils.leftPad(val, colWidths[i + 1]));
}
else {
writer.print(StringUtils.rightPad(val, colWidths[i + 1]));
}
}
writer.println(" |");
}
// Closing separator
writer.println(separator);
writer.flush();
}
private boolean isDouble(String val)
{
try {
double d = Double.parseDouble(val); // TODO: A bit of a hack; use Regex instead?
}
catch (NumberFormatException ex) {
return false;
}
return true;
}
};
}
public StreamWriter getTWikiWriter()
{
return new StreamWriter()
{
protected PrintWriter writer;
@Override
public void write(OutputStream aStream)
throws Exception
{
writer = new PrintWriter(new OutputStreamWriter(aStream, "UTF-8"));
if (compact && rows.size() > 0) {
String firstRowId = getRowIds()[0];
String[] colIds = getCompactColumnIds(true);
for (String colId : colIds) {
writer.print("| *");
writer.print(colId.replace('|', ' '));
writer.print("* | ");
writer.print(getValueAsString(firstRowId, colId).replace('|', ' '));
writer.println(" |");
}
writer.println();
writer.println();
}
String[] colIds = compact ? getCompactColumnIds(false) : getColumnIds();
String[] buf = new String[colIds.length + 1];
{
int i = 1;
buf[0] = "ID";
for (String col : colIds) {
buf[i] = col.replace('|', ' ');
i++;
}
}
printHeaderRow(buf);
for (String rowId : getRowIds()) {
buf[0] = rowId;
int i = 1;
for (String colId : colIds) {
buf[i] = getValueAsString(rowId, colId).replace('|', ' ');
i++;
}
printRow(buf);
}
writer.flush();
}
protected void printHeaderRow(String[] aHeaders)
{
writer.print("| *");
writer.print(StringUtils.join(aHeaders, "* | *"));
writer.println("* |");
}
protected void printRow(String[] aCells)
{
writer.print("| !");
writer.print(StringUtils.join(aCells, " | "));
writer.println(" |");
}
};
}
/**
* Returns a LaTeX writer to write the FlexTable to a Latex file. (without rounding any figures)
*
*/
public StreamWriter getLatexWriter()
{
return this.getLatexWriter(-1, -1);
}
/**
* Returns a LaTeX writer to write the FlexTable to a Latex file. To get a writer for a
* transposed version of the table, call table.transpose() prior to retrieving the writer.
*
* @param decimalPlacesForDouble
* How many decimal places should double values have; if set to -1, the values won't
* be rounded.
* @param decimalPlacesForPercentages
* How many decimal places should percentage values have; if set to -1, the values
* won't be rounded.
*/
public StreamWriter getLatexWriter(final int decimalPlacesForDouble,
final int decimalPlacesForPercentages)
{
return new StreamWriter()
{
@Override
public void write(OutputStream aStream)
throws Exception
{
PrintWriter writer = new PrintWriter(new OutputStreamWriter(aStream, "UTF-8"));
writer.print("\\begin{tabular}");
String[] colIds = getColumnIds();
writer.print("{");
for (int i = 0; i <= colIds.length; i++) {
writer.print(" l"); // TODO: Pass column alignment as parameter
}
writer.println(" }");
writer.print("\\small");
String[] buf = new String[colIds.length + 1];
{
int i = 1;
buf[0] = "ID";
for (String col : colIds) {
buf[i] = escapeForLatex(col.replace('|', ' '));
i++;
}
}
writer.println("\\hline");
writer.print(StringUtils.join(buf, " & "));
writer.println("\\\\");
for (String rowId : getRowIds()) {
String rowVal = doStringReplace(rowId);
buf[0] = escapeForLatex(rowVal);
int i = 1;
for (String colId : colIds) {
String val = getValueAsString(rowId, colId);
val = convertNumbers(decimalPlacesForDouble, decimalPlacesForPercentages,
val);
buf[i] = escapeForLatex(val); // TODO: Move to util class? Which one?
i++;
}
writer.print(StringUtils.join(buf, " & "));
writer.println("\\\\");
}
writer.println("\\hline");
writer.println("\\end{tabular}");
writer.flush();
}
private String convertNumbers(final int decimalPlacesForDouble,
final int decimalPlacesForPercentages, String val)
{
if (decimalPlacesForPercentages != -1 && isPercentage(val)) {
val = doRoundDouble(val, decimalPlacesForPercentages);
}
else if (decimalPlacesForDouble != -1 && isDouble(val)) {
val = doRoundDouble(val, decimalPlacesForDouble);
}
return val;
}
private String doStringReplace(String val)
{
// TODO MW: Make all this configurable
val = val.replace("de.tudarmstadt.ukp.dkpro.tc.core.task.", "");
val = val.replace("de.tudarmstadt.ukp.dkpro.tc.weka.task.", "");
return val;
}
private String doRoundDouble(String val, int decimalPlaces)
{
double d = Double.parseDouble(val);
int temp = (int) (d * Math.pow(10, decimalPlaces)); // TODO: Could this cause
// rounding problems?
Double newD = (temp) / Math.pow(10, decimalPlaces);
return newD.toString();
}
private boolean isDouble(String val)
{
try {
double d = Double.parseDouble(val); // TODO: A bit of a hack; use Regex instead?
}
catch (NumberFormatException ex) {
return false;
}
return true;
}
private boolean isPercentage(String val)
{
// TODO: Implement
return false;
}
private String escapeForLatex(String val)
{
val = val.replace("_", "\\_");
return val;
}
};
}
/**
* Method to transpose the data in the table, turning columns into rows and vice versa.
*/
public void transposeTable()
{
LinkedHashMap<String, Object> newColumns = new LinkedHashMap<>();
Map<String, Map<String, V>> newRows = new LinkedHashMap<>();
for (String columnHeader : columns.keySet()) {
String newRowID = columnHeader;
Map<String, V> row = new LinkedHashMap<>();
Map<String, V> oldRow;
for (String rowID : rows.keySet()) {
if (!newColumns.containsKey(rowID)) {
newColumns.put(rowID, PRESENT);
}
oldRow = rows.get(rowID);
V value = oldRow.get(newRowID);
row.put(rowID, value);
}
newRows.put(newRowID, row);
}
columns = newColumns;
rows = newRows;
}
public StreamWriter getCsvWriter()
{
return new StreamWriter()
{
@Override
public void write(OutputStream aStream)
throws Exception
{
String[] colIds = getColumnIds();
CSVWriter writer = new CSVWriter(new OutputStreamWriter(aStream, "UTF-8"));
String[] buf = new String[FlexTable.this.columns.size() + 1];
{
int i = 1;
buf[0] = "ID";
for (String col : colIds) {
buf[i] = col;
i++;
}
}
writer.writeNext(buf);
for (String rowId : getRowIds()) {
buf[0] = rowId;
int i = 1;
for (String colId : colIds) {
buf[i] = getValueAsString(rowId, colId);
i++;
}
writer.writeNext(buf);
}
writer.flush();
}
};
}
public StreamReader getCsvReader()
{
return new StreamReader()
{
@Override
public void read(InputStream aStream)
throws IOException
{
try {
CSVReader reader = new CSVReader(new InputStreamReader(aStream, "UTF-8"));
String[] headers = reader.readNext();
Method converter = FlexTable.this.dataClass.getMethod("valueOf", String.class);
String[] data;
while ((data = reader.readNext()) != null) {
Map<String, V> row = new LinkedHashMap<String, V>();
for (int i = 1; i < headers.length; i++) {
@SuppressWarnings("unchecked")
V value = (V) converter.invoke(null, data[i]);
row.put(headers[i], value);
}
addRow(data[0], row);
}
}
catch (IOException e) {
throw e;
}
catch (NoSuchMethodException e) {
throw new IOException("Data class "+FlexTable.this.dataClass.getName()+" does not have a "+
"public static Object valueOf(String) method - unable unmarshall the "+
"data.");
}
catch (Exception e) {
throw new IOException(e);
}
}
};
}
public StreamWriter getExcelWriter()
{
return new StreamWriter()
{
@Override
public void write(OutputStream aStream)
throws Exception
{
String[] colIds = FlexTable.this.compact ? getCompactColumnIds(false)
: getColumnIds();
Workbook wb = new HSSFWorkbook();
Sheet sheet = wb.createSheet("Summary");
PrintSetup printSetup = sheet.getPrintSetup();
printSetup.setLandscape(true);
sheet.setFitToPage(true);
sheet.setHorizontallyCenter(true);
// Header row
{
Row row = sheet.createRow(0);
Cell rowIdCell = row.createCell(0);
rowIdCell.setCellValue("ID");
int colNum = 1;
for (String colId : colIds) {
Cell cell = row.createCell(colNum);
cell.setCellValue(colId);
colNum++;
}
}
// Body rows
{
int rowNum = 1;
for (String rowId : getRowIds()) {
Row row = sheet.createRow(rowNum);
Cell rowIdCell = row.createCell(0);
rowIdCell.setCellValue(rowId);
int colNum = 1;
for (String colId : colIds) {
Cell cell = row.createCell(colNum);
String value = getValueAsString(rowId, colId);
try {
cell.setCellValue(Double.valueOf(value));
}
catch (NumberFormatException e) {
cell.setCellValue(value);
}
colNum++;
}
rowNum++;
}
}
wb.write(aStream);
}
};
}
}