org.xwiki.rendering.internal.macro.chart.source.table.AbstractTableBlockDataSource.java Source code

Java tutorial

Introduction

Here is the source code for org.xwiki.rendering.internal.macro.chart.source.table.AbstractTableBlockDataSource.java

Source

/*
 * See the NOTICE file distributed with this work for additional
 * information regarding copyright ownership.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.xwiki.rendering.internal.macro.chart.source.table;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.inject.Inject;
import javax.inject.Named;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.xwiki.chart.dataset.DatasetType;
import org.xwiki.rendering.block.TableBlock;
import org.xwiki.rendering.block.TableCellBlock;
import org.xwiki.rendering.block.TableRowBlock;
import org.xwiki.rendering.internal.macro.chart.source.AbstractDataSource;
import org.xwiki.rendering.internal.macro.chart.source.SimpleChartModel;
import org.xwiki.rendering.macro.MacroExecutionException;
import org.xwiki.rendering.renderer.BlockRenderer;
import org.xwiki.rendering.renderer.printer.DefaultWikiPrinter;
import org.xwiki.rendering.renderer.printer.WikiPrinter;
import org.xwiki.rendering.transformation.MacroTransformationContext;

/**
 * Data source that extracts values from a table (in any syntax that supports tables).  For example in xwiki/2.1 syntax:
 *
 * <p/>
 * <code><pre>
 * |=           |= column label 1 |= column label 2  
 * | row label 1| 11              |  12
 * | row label 2| 21              |  22
 * </pre></code>
 *
 * @version $Id: 7d3837a94a4cf8c46e52435cec2cdc2bf282c421 $
 * @since 4.2M1
 */
public abstract class AbstractTableBlockDataSource extends AbstractDataSource {
    /**
     * The number of letters that can be used in the column identifier.
     */
    private static final int LETTER_RANGE_LENGTH = 'Z' - 'A' + 1;

    /**
     * Identifies the data range to be used for plotting.
     */
    private static final String RANGE_PARAM = "range";

    /**
     * Indicates how the table values should be mapped to the dataset.
     */
    private static final String SERIES_PARAM = "series";

    /**
     * The columns value for the series parameter.
     */
    private static final String SERIES_COLUMNS = "columns";

    /**
     * The rows value for the series parameter.
     */
    private static final String SERIES_ROWS = "rows";

    /**
     * No-limit indicator symbol in ranges.
     */
    private static final String NO_LIMIT_SYMBOL = ".";

    /**
     * Pattern matching the cell range.
     */
    private static final Pattern RANGE_PATTERN = Pattern
            .compile("^([A-Z]+|\\.)([0-9]+|\\.)-([A-Z]+|\\.)([0-9]+|\\.)$");

    /**
     * The name of the category dataset.
     */
    private static final String CATEGORY_DATASET = "category";

    /**
     * The name of the time series dataset.
     */
    private static final String TIME_SERIES_DATASET = "timeseries";

    /**
     * The name of the pie dataset.
     */
    private static final String PIE_DATASET = "pie";

    /**
     * The default dataset.
     */
    private static final String DEFAULT_DATASET = CATEGORY_DATASET;

    /**
     * The range parameter.
     */
    private String range;

    /**
     * The series parameter.
     */
    private String series;

    /**
     * Used to convert cell blocks in plain text so that it can be converted to numbers.
     */
    @Inject
    @Named("plain/1.0")
    private BlockRenderer plainTextBlockRenderer;

    @Override
    public void buildDataset(String macroContent, Map<String, String> parameters,
            MacroTransformationContext context) throws MacroExecutionException {
        validateParameters(parameters);

        TableBlock tableBlock = getTableBlock(macroContent, context);

        int[] dataRange = getDataRange(tableBlock);

        TableDatasetBuilder datasetBuilder;
        setChartModel(new SimpleChartModel());

        switch (getDatasetType()) {
        case CATEGORY:
            datasetBuilder = new TableCategoryDatasetBuilder();
            break;
        case PIE:
            datasetBuilder = new TablePieDatasetBuilder();
            break;
        case TIMETABLE_XY:
            datasetBuilder = new TableTimeTableXYDatasetBuilder();
            break;
        default:
            throw new MacroExecutionException(
                    String.format("Unsupported dataset type [%s]", getDatasetType().getName()));
        }

        setAxes();

        datasetBuilder.setLocaleConfiguration(getLocaleConfiguration());
        datasetBuilder.setParameters(parameters);

        if (SERIES_COLUMNS.equals(series)) {
            datasetBuilder.setTranspose(true);
        }

        buildDataset(tableBlock, dataRange, datasetBuilder);

        setDataset(datasetBuilder.getDataset());
    }

    /**
     * @param tableBlock The table block to parse.
     * @param startRow The first row to include.
     * @param endRow The last row to include.
     * @param startColumn The first column to include.
     * @param datasetBuilder The dataset builder.
     * @throws MacroExecutionException if there are any errors in the table.
     */
    private void getRowKeys(TableBlock tableBlock, int startRow, int endRow, int startColumn,
            TableDatasetBuilder datasetBuilder) throws MacroExecutionException {

        datasetBuilder.setNumberOfRows(endRow - startRow + 1);

        if (startColumn > 0) {
            Set<String> rowKeySet = new HashSet<String>();
            for (int i = startRow; i <= endRow; i++) {
                TableRowBlock tableRow = (TableRowBlock) tableBlock.getChildren().get(i);
                String key = cellContentAsString((TableCellBlock) tableRow.getChildren().get(startColumn - 1));
                datasetBuilder.setRowHeading(i - startRow, key);
            }
        } else {
            for (int i = startRow; i <= endRow; i++) {
                datasetBuilder.setRowHeading(i - startRow, "R" + i);
            }
        }
    }

    /**
     * @param tableBlock The table block to parse.
     * @param startColumn The first column to include.
     * @param endColumn The last column to include.
     * @param startRow The first row to include.
     * @param datasetBuilder The dataset builder.
     * @throws MacroExecutionException if there are any errors in the table.
     */
    private void getColumnKeys(TableBlock tableBlock, int startColumn, int endColumn, int startRow,
            TableDatasetBuilder datasetBuilder) throws MacroExecutionException {
        datasetBuilder.setNumberOfColumns(endColumn - startColumn + 1);

        if (startRow > 0) {
            TableRowBlock tableRow = (TableRowBlock) tableBlock.getChildren().get(startRow - 1);
            for (int i = startColumn; i <= endColumn; i++) {
                String key = cellContentAsString((TableCellBlock) tableRow.getChildren().get(i));
                datasetBuilder.setColumnHeading(i - startColumn, key);
            }
        } else {
            for (int i = startColumn; i <= endColumn; i++) {
                datasetBuilder.setColumnHeading(i - startColumn, "C" + i);
            }
        }
    }

    /**
     * Build a category dataset.
     *
     * @param tableBlock The table block to parse.
     * @param dataRange The data range.
     * @param datasetBuilder The dataset builder.
     * @throws MacroExecutionException if there are any errors.
     */
    private void buildDataset(TableBlock tableBlock, int[] dataRange, TableDatasetBuilder datasetBuilder)
            throws MacroExecutionException {
        int startRow = dataRange[0];
        int startColumn = dataRange[1];
        int endRow = dataRange[2];
        int endColumn = dataRange[3];

        if (startRow == 0 && datasetBuilder.forceRowHeadings()) {
            startRow = 1;
        }

        if (startColumn == 0 && datasetBuilder.forceColumnHeadings()) {
            startColumn = 1;
        }

        getRowKeys(tableBlock, startRow, endRow, startColumn, datasetBuilder);

        getColumnKeys(tableBlock, startColumn, endColumn, startRow, datasetBuilder);

        for (int i = startRow; i <= endRow; i++) {
            if (i < tableBlock.getChildren().size()) {
                TableRowBlock tableRow = (TableRowBlock) tableBlock.getChildren().get(i);
                for (int j = startColumn; j <= endColumn; j++) {
                    if (j < tableRow.getChildren().size()) {
                        Number value = cellContentAsNumber((TableCellBlock) tableRow.getChildren().get(j));
                        datasetBuilder.setValue(i - startRow, j - startColumn, value);
                    } else {
                        throw new MacroExecutionException("Data range (columns) overflow.");
                    }
                }
            } else {
                throw new MacroExecutionException("Data range (rows) overflow.");
            }
        }
    }

    /**
     * Converts a column identifier ('G') into a column index number (6).
     *
     * @param identifier the cell identifier, containing a column ([A-Z]{1,2})
     * @return the column number extracted from the identifier
     */
    protected static Integer getColumnNumberFromIdentifier(String identifier) {
        if (NO_LIMIT_SYMBOL.equals(identifier)) {
            return null;
        }
        int i = 0;
        int result = -1;
        char j;
        while (i < identifier.length()) {
            j = identifier.charAt(i++);
            if (!Character.isUpperCase(j)) {
                break;
            }
            result = (result + 1) * LETTER_RANGE_LENGTH + j - 'A';
        }
        return result;
    }

    /**
     * Converts a row identifier ('6') into a column index number (6).
     *
     * @param identifier the cell identifier, containing a column.
     * @return the column number extracted from the identifier
     */
    private static Integer getRowNumberFromIdentifier(String identifier) {
        if (NO_LIMIT_SYMBOL.equals(identifier)) {
            return null;
        }
        return Integer.parseInt(identifier) - 1;
    }

    /**
     * Tries to parse the cell content as a number.
     *
     * @param cell the {@link TableCellBlock}.
     * @return cell content parsed as a {@link Number}.
     * @throws MacroExecutionException if the cell does not represent a number.
     */
    private Number cellContentAsNumber(TableCellBlock cell) throws MacroExecutionException {
        String stringContent = cellContentAsString(cell);
        try {
            return NumberUtils.createNumber(StringUtils.trim(stringContent));
        } catch (NumberFormatException ex) {
            throw new MacroExecutionException(String.format("Invalid number: [%s].", stringContent));
        }
    }

    /**
     * Renders the cell content as a string.
     *
     * @param cell the {@link TableCellBlock}.
     * @return cell content rendered as a string.
     */
    private String cellContentAsString(TableCellBlock cell) {
        WikiPrinter printer = new DefaultWikiPrinter();
        this.plainTextBlockRenderer.render(cell.getChildren(), printer);
        return printer.toString();
    }

    /**
     * Parses the range parameter and return an array on the form [startRow, startColumn, endRow, endColumn].  Any
     * element in the array may be {@code null} to indicate no bound.
     *
     * @return An array as described above.
     * @throws MacroExecutionException if the range parameter cannot be parsed.
     */
    private Integer[] getDataRangeFromParameter() throws MacroExecutionException {
        Integer startColumn = null;
        Integer endColumn = null;
        Integer startRow = null;
        Integer endRow = null;

        if (range != null) {

            Matcher m = RANGE_PATTERN.matcher(range);

            if (!m.matches()) {
                throw new MacroExecutionException(String.format("Invalid range specification: [%s].", range));
            }

            startColumn = getColumnNumberFromIdentifier(m.group(1));
            startRow = getRowNumberFromIdentifier(m.group(2));
            endColumn = getColumnNumberFromIdentifier(m.group(3));
            endRow = getRowNumberFromIdentifier(m.group(4));

            if (startColumn != null && endColumn != null && startColumn > endColumn) {
                throw new MacroExecutionException(String
                        .format("Invalid data range, end column mustn't come before start column: [%s].", range));
            }

            if (startRow != null && endRow != null && startRow > endRow) {
                throw new MacroExecutionException(
                        String.format("Invalid data range, end row mustn't come before start row: [%s].", range));
            }
        }

        return new Integer[] { startRow, startColumn, endRow, endColumn };
    }

    /**
     * Calculates the data-range that is to be used for plotting the chart.
     *
     * @param tableBlock the {@link TableBlock}.
     * @return an integer array consisting of start-row, start-column, end-row and end-column of the data range.
     * @throws MacroExecutionException if it's not possible to determine the data range correctly.
     */
    protected int[] getDataRange(TableBlock tableBlock) throws MacroExecutionException {

        Integer[] r = getDataRangeFromParameter();

        int rowCount = tableBlock.getChildren().size();
        if (rowCount > 0) {
            TableRowBlock firstRow = (TableRowBlock) tableBlock.getChildren().get(0);
            int columnCount = firstRow.getChildren().size();
            if (columnCount > 0) {
                return new int[] { r[0] != null ? r[0] : 0, r[1] != null ? r[1] : 0,
                        r[2] != null ? r[2] : rowCount - 1, r[3] != null ? r[3] : columnCount - 1 };
            }
        }

        throw new MacroExecutionException("Data table is incomplete.");
    }

    @Override
    public void validateDatasetType() throws MacroExecutionException {
        super.validateDatasetType();

        switch (getDatasetType()) {
        case CATEGORY:
            break;
        case PIE:
            break;
        case TIMETABLE_XY:
            break;
        default:
            throw new MacroExecutionException(String.format(
                    "Dataset type [%s] is not supported by the table data source.", getDatasetType().getName()));
        }
    }

    /**
     * Returns the {@link TableBlock} which contains the data to be plotted.
     *
     * @param macroContent macro content.
     * @param context the macro transformation context, used for example to find out the current document reference
     * @return the {@link TableBlock} containing the data to be plotted.
     * @throws MacroExecutionException if it's not possible to locate the {@link TableBlock} specified by the user.
     */
    protected abstract TableBlock getTableBlock(String macroContent, MacroTransformationContext context)
            throws MacroExecutionException;

    @Override
    protected boolean setParameter(String key, String value) throws MacroExecutionException {
        if (RANGE_PARAM.equals(key)) {
            range = value;
            return true;
        }
        if (SERIES_PARAM.equals(key)) {
            series = value;
            return true;
        }
        return false;
    }

    @Override
    protected void validateParameters() throws MacroExecutionException {
        if (series == null) {
            // Backwards compliancy: The "series" parameter doesn't really make sense for category datasets, but setting
            // it to "columns" indicates that the table should be transposed.  Adding to oddities, it also takes on
            // different default values depending on the dataset type.
            series = getDatasetType() == DatasetType.CATEGORY ? SERIES_ROWS : SERIES_COLUMNS;
        } else {
            if (!(SERIES_COLUMNS.equals(series) || SERIES_ROWS.equals(series))) {
                throw new MacroExecutionException(
                        String.format("Unsupported value for parameter [%s]: [%s]", SERIES_PARAM, series));
            }
        }
    }
}