All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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:
    *
  1. {@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
  2. *
  3. {@link #sizeConstraints size constraints} are then applied, which decide how * much column real estate to allocate to cells
  4. *
  5. {@link #wrappers text wrapping policies} are applied once the column sizes * are known
  6. *
  7. finally, {@link #aligners alignment} strategies actually render * text as a series of space-padded strings that draw nicely on screen.
  8. *
* 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