org.springframework.shell.table.Table Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2017 the original author or authors.
*
* 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
*
* https://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.springframework.shell.table;
import static org.springframework.shell.table.BorderSpecification.NONE;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.shell.TerminalSizeAware;
/**
* This is the central API for table rendering. A Table object is constructed with a given
* TableModel, which holds raw table contents. Its rendering logic is then altered by applying
* various customizations, in a fashion very similar to what is used e.g. in a spreadsheet
* program:
* - {@link #formatters formatters} know how to derive character data out of raw data. For
* example, numbers are
* formatted according to a Locale, or Maps are emitted as a series of {@literal key=value} lines
* - {@link #sizeConstraints size constraints} are then applied, which decide how
* much column real estate to allocate to cells
* - {@link #wrappers text wrapping policies} are applied once the column sizes
* are known
* - finally, {@link #aligners alignment} strategies actually render
* text as a series of space-padded strings that draw nicely on screen.
*
* All those customizations are applied selectively on the Table cells thanks to a {@link CellMatcher}: One can
* decide to right pad column number 3, or to format in a certain way all instances of {@literal java.util.Map}.
*
* Of course, all of those customizations often work hand in hand, and not all combinations make sense:
* one needs to anticipate the fact that text will be split using the ' ' (space) character to properly
* calculate column sizes.
* @author Eric Bottard
*/
public class Table implements TerminalSizeAware {
private final int rows;
private final int columns;
private TableModel model;
private Map formatters = new LinkedHashMap();
private Map sizeConstraints = new LinkedHashMap();
private Map wrappers = new LinkedHashMap();
private Map aligners = new LinkedHashMap();
private List borderSpecifications = new ArrayList();
/**
* Construct a new Table with the given model and customizers.
* The passed in LinkedHashMap should be in reverse-insertion order (i.e. the first CellMatcher
* found in iteration order will "win").
*
* @see TableBuilder#build()
*/
/*package*/ Table(TableModel model,
LinkedHashMap formatters,
LinkedHashMap sizeConstraints,
LinkedHashMap wrappers,
LinkedHashMap aligners,
List borderSpecifications) {
this.model = model;
this.formatters = formatters;
this.sizeConstraints = sizeConstraints;
this.wrappers = wrappers;
this.aligners = aligners;
this.borderSpecifications = borderSpecifications;
rows = model.getRowCount();
columns = model.getColumnCount();
}
public TableModel getModel() {
return model;
}
public String render(int totalAvailableWidth) {
StringBuilder result = new StringBuilder();
int[] cellHeights = new int[rows];
int[] cellWidths;
int[] minCellWidths = new int[columns];
int[] maxCellWidths = new int[columns];
String[][][] subLines = new String[rows][columns][];
Borders borders = new Borders();
int widthAvailableForContents = totalAvailableWidth - borders.getNumberOfVerticalBorders();
// First, compute desired column widths
for (int row = 0; row < rows; row++) {
for (int column = 0; column < columns; column++) {
Object value = model.getValue(row, column);
String[] lines = getFormatter(row, column).format(value);
subLines[row][column] = lines;
SizeConstraints.Extent extent = getSizeConstraints(row, column).width(lines, widthAvailableForContents, columns);
minCellWidths[column] = Math.max(minCellWidths[column], extent.min);
maxCellWidths[column] = Math.max(maxCellWidths[column], extent.max);
}
}
cellWidths = computeColumnWidths(widthAvailableForContents, minCellWidths, maxCellWidths);
// Now that widths are known, apply wrapping & render
for (int row = 0; row < rows; row++) {
for (int column = 0; column < columns; column++) {
subLines[row][column] = getWrapper(row, column).wrap(subLines[row][column], cellWidths[column]);
cellHeights[row] = Math.max(cellHeights[row], subLines[row][column].length);
}
for (int column = 0; column < columns; column++) {
for (Map.Entry kv : aligners.entrySet()) {
if (kv.getKey().matches(row, column, model)) {
subLines[row][column] = kv.getValue().align(subLines[row][column], cellWidths[column], cellHeights[row]);
}
}
}
}
for (int row = 0; row < rows; row++) {
// TOP CELL BORDER
int before = result.length();
for (int column = 0; column < columns; column++) {
borders.paintCorner(row, column, result);
borders.paintHorizontal(row, column, cellWidths[column], result);
}
borders.paintCorner(row, columns, result);
if (result.length() > before) {
result.append('\n');
}
for (int subRow = 0; subRow < cellHeights[row]; subRow++) {
for (int column = 0; column < columns; column++) {
// LEFT CELL BORDER
borders.paintVertical(row, column, result);
String[] lines = subLines[row][column];
result.append(lines[subRow]);
}
// TABLE RIGHT BORDER
borders.paintVertical(row, columns, result);
result.append("\n");
}
}
// TABLE BOTTOM BORDER
int before = result.length();
for (int column = 0; column < columns; column++) {
borders.paintCorner(rows, column, result);
borders.paintHorizontal(rows, column, cellWidths[column], result);
}
// TABLE BOTTOM RIGHT CORNER
borders.paintCorner(rows, columns, result);
if (result.length() > before) {
result.append('\n');
}
return result.toString();
}
private int[] computeColumnWidths(int availableWidth, int[] minCellWidths, int[] maxCellWidths) {
int[] cellWidths;
int minTableWidth = 0, maxTableWidth = 0;
for (int column = 0; column < columns; column++) {
minTableWidth += minCellWidths[column];
maxTableWidth += maxCellWidths[column];
}
// Can use max desired width
if (maxTableWidth <= availableWidth) {
cellWidths = maxCellWidths;
} // will overflow
else if (minTableWidth >= availableWidth) {
cellWidths = minCellWidths;
} // Redistribute nicely
else {
int W = availableWidth - minTableWidth;
int D = maxTableWidth - minTableWidth;
cellWidths = new int[columns];
for (int column = 0; column < columns; column++) {
cellWidths[column] = minCellWidths[column] + W * (maxCellWidths[column] - minCellWidths[column]) / D;
}
}
return cellWidths;
}
private TextWrapper getWrapper(int row, int column) {
for (Map.Entry kv : wrappers.entrySet()) {
if (kv.getKey().matches(row, column, model)) {
return kv.getValue();
}
}
throw new AssertionError("Can't be reached thanks to the whole-table default");
}
private SizeConstraints getSizeConstraints(int row, int column) {
for (Map.Entry kv : sizeConstraints.entrySet()) {
if (kv.getKey().matches(row, column, model)) {
return kv.getValue();
}
}
throw new AssertionError("Can't be reached thanks to the whole-table default");
}
private Formatter getFormatter(int row, int column) {
for (Map.Entry kv : formatters.entrySet()) {
if (kv.getKey().matches(row, column, model)) {
return kv.getValue();
}
}
throw new AssertionError("Can't be reached thanks to the whole-table default");
}
/**
* An instance of this class knows where to paint border glyphs.
*
* In all instance arrays, 'row' and 'column' are actually indices in-between
* table rows and columns. Hence, sizes are larger by one.
* @author Eric Bottard
*/
private class Borders {
/**
* Glyph to paint a vertical line at row,col.
*/
private char[][] verticals;
/**
* Glyph to paint a horizontal line at row,col.
*/
private char[][] horizontals;
/**
* The type of corner, if any, to paint at row,col.
*/
private char[][] corners;
/**
* True if at least one vertical bar exists in that col.
*/
private boolean[] vFillers;
/**
* True if at least one horizontal bar exists in that row.
*/
private boolean[] hFillers;
public Borders() {
verticals = new char[rows][columns + 1];
horizontals = new char[rows + 1][columns];
corners = new char[rows + 1][columns + 1];
vFillers = new boolean[columns + 1];
hFillers = new boolean[rows + 1];
init();
}
private void init() {
for (int row = 0; row <= rows; row++) {
for (int column = 0; column <= columns; column++) {
for (BorderSpecification bs : borderSpecifications) {
if (row < rows) {
char verticalThere = bs.verticals(row, column);
if (verticalThere != BorderStyle.NONE) {
this.verticals[row][column] = verticalThere;
vFillers[column] |= true;
}
}
if (column < columns) {
char horizontalThere = bs.horizontals(row, column);
if (horizontalThere != BorderStyle.NONE) {
this.horizontals[row][column] = horizontalThere;
hFillers[row] |= true;
}
}
}
}
}
// Compute corners when horizontals & verticals intersect
for (int row = 0; row <= rows; row++) {
for (int column = 0; column <= columns; column++) {
char left = (column - 1 >= 0) ? horizontals[row][column - 1] : NONE;
char right = (column < columns) ? horizontals[row][column] : NONE;
char above = (row - 1 >= 0) ? verticals[row - 1][column] : NONE;
char below = (row < rows) ? verticals[row][column] : NONE;
corners[row][column] = BorderStyle.intersection(above, below, left, right);
}
}
}
private void paintCorner(int row, int column, StringBuilder stringBuilder) {
if (corners[row][column] != NONE) {
stringBuilder.append(corners[row][column]);
} // If there is a border in same row|column, paint filler
else if (vFillers[column] && hFillers[row]) {
stringBuilder.append(' ');
}
}
private void paintVertical(int row, int column, StringBuilder stringBuilder) {
if (verticals[row][column] != NONE) {
stringBuilder.append(verticals[row][column]);
}
else if (vFillers[column]) {
stringBuilder.append(' ');
}
}
private void paintHorizontal(int row, int column, int width, StringBuilder stringBuilder) {
if (horizontals[row][column] != NONE) {
for (int i = 0; i < width; i++) {
stringBuilder.append(horizontals[row][column]);
}
}
else if (hFillers[row]) {
for (int i = 0; i < width; i++) {
stringBuilder.append(' ');
}
}
}
/**
* Return the number of vertical borders, and hence the space consumed by those.
*/
public int getNumberOfVerticalBorders() {
int result = 0;
for (boolean b : vFillers) {
if (b) {
result++;
}
}
return result;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy