org.dkpro.lab.reporting.FlexTable.java Source code

Java tutorial

Introduction

Here is the source code for org.dkpro.lab.reporting.FlexTable.java

Source

/*******************************************************************************
 * Copyright 2011
 * Ubiquitous Knowledge Processing (UKP) Lab
 * Technische Universitt 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);
            }
        };
    }
}