|
@@ -0,0 +1,310 @@
|
|
|
|
+package com.wecise.odb.table;
|
|
|
|
+
|
|
|
|
+import java.io.ByteArrayOutputStream;
|
|
|
|
+import java.io.IOException;
|
|
|
|
+import java.io.OutputStream;
|
|
|
|
+import java.io.PrintStream;
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
+import java.util.LinkedList;
|
|
|
|
+import java.util.List;
|
|
|
|
+
|
|
|
|
+import static com.wecise.odb.table.Utils.*;
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+/**
|
|
|
|
+ * Created by fabmars on 17/09/16.
|
|
|
|
+ */
|
|
|
|
+public abstract class ConsoleTable<R> {
|
|
|
|
+
|
|
|
|
+ public static final String BORDER_LEFT = "|";
|
|
|
|
+ public static final String COLUMN_SEPARATOR = BORDER_LEFT;
|
|
|
|
+ public static final String BORDER_RIGHT = BORDER_LEFT;
|
|
|
|
+ public static final char BORDER_PADDING = ' ';
|
|
|
|
+ public static final char BORDER_TOP_BOTTOM = '=';
|
|
|
|
+ public static final char BORDER_LINE = '-';
|
|
|
|
+
|
|
|
|
+ private boolean headers;
|
|
|
|
+
|
|
|
|
+ Boolean preProcessed = false;
|
|
|
|
+ private int[] widthsCache;
|
|
|
|
+ private String[] headerRenderCache;
|
|
|
|
+ private List<String[]> cellRenderCache;
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ public ConsoleTable(boolean headers) {
|
|
|
|
+ this.headers = headers;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public abstract int getColumnCount();
|
|
|
|
+ public abstract int getRowCount();
|
|
|
|
+
|
|
|
|
+ public boolean isHeaders() {
|
|
|
|
+ return headers;
|
|
|
|
+ }
|
|
|
|
+ public abstract Object getHeader(int column);
|
|
|
|
+
|
|
|
|
+ public abstract R getRow(int rowNum);
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Tells whether a given row should be rendered.
|
|
|
|
+ * For instance you might not want to render null rows
|
|
|
|
+ * @param rowObject
|
|
|
|
+ * @param rowNum
|
|
|
|
+ * @return
|
|
|
|
+ */
|
|
|
|
+ public boolean isRenderable(R rowObject, int rowNum) {
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * gets a property of an object aimed at one column of the table
|
|
|
|
+ * @param row a non-null row object
|
|
|
|
+ * @param colNum the target table column
|
|
|
|
+ * @return the cell value
|
|
|
|
+ */
|
|
|
|
+ public abstract Object getCell(R row, int colNum);
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ public final int getRenderedWidth(int colNum) {
|
|
|
|
+ ensurePreProcessed(false); // Not necessary per se, just in case someone calls this method independently of stream()
|
|
|
|
+ return widthsCache[colNum];
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public final String getRenderedHeader(int colNum) {
|
|
|
|
+ ensurePreProcessed(false); // Not necessary per se, just in case someone calls this method independently of stream()
|
|
|
|
+ String headerString = headerRenderCache[colNum];
|
|
|
|
+ return pad(headerString, getRenderedWidth(colNum), getAlignment(colNum));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private String getRenderedCell(int rowNum, int colNum) {
|
|
|
|
+ ensurePreProcessed(false); // Not necessary per se, just in case someone calls this method independently of stream()
|
|
|
|
+ String cellString = cellRenderCache.get(rowNum)[colNum];
|
|
|
|
+ return pad(cellString, getRenderedWidth(colNum), getAlignment(colNum));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected synchronized final void ensurePreProcessed(boolean force) {
|
|
|
|
+ if(!preProcessed || force) {
|
|
|
|
+ int columnCount = getColumnCount();
|
|
|
|
+ widthsCache = new int[columnCount];
|
|
|
|
+ headerRenderCache = new String[columnCount];
|
|
|
|
+
|
|
|
|
+ if (isHeaders()) {
|
|
|
|
+ for (int colNum = 0; colNum < columnCount; colNum++) {
|
|
|
|
+ String renderedHeader = safeRenderHeader(getHeader(colNum), colNum);
|
|
|
|
+ headerRenderCache[colNum] = renderedHeader;
|
|
|
|
+ widthsCache[colNum] = renderedHeader.length();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ int rowCount = getRowCount();
|
|
|
|
+ cellRenderCache = new LinkedList<>();
|
|
|
|
+ for (int rowNum = 0; rowNum < rowCount; rowNum++) {
|
|
|
|
+ R row = getRow(rowNum);
|
|
|
|
+ if (isRenderable(row, rowNum)) {
|
|
|
|
+ String[] rowCache = new String[columnCount];
|
|
|
|
+ for (int colNum = 0; colNum < columnCount; colNum++) {
|
|
|
|
+ Object cellValue = row != null ? getCell(row, colNum) : NonExistent.instance;
|
|
|
|
+ String renderedCell = safeRenderCell(cellValue, rowNum, colNum);
|
|
|
|
+ rowCache[colNum] = renderedCell;
|
|
|
|
+
|
|
|
|
+ int len = renderedCell.length();
|
|
|
|
+ if (widthsCache[colNum] < len) {
|
|
|
|
+ widthsCache[colNum] = len;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ cellRenderCache.add(rowCache);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ preProcessed = true;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ public Align getAlignment(int colNum) {
|
|
|
|
+ return Align.RIGHT;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public ConsoleHeaderRenderer getHeaderRenderer(int colNum) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public ConsoleCellRenderer getCellRenderer(int rowNum, int colNum) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public Class<?> getColumnClass(int column) {
|
|
|
|
+ return Object.class;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public ConsoleHeaderRenderer getDefaultHeaderRenderer(Class<?> clazz) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public ConsoleCellRenderer getDefaultCellRenderer(Class<?> clazz) {
|
|
|
|
+ return null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public String toString() {
|
|
|
|
+ try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
|
|
|
+ stream(baos);
|
|
|
|
+ return baos.toString();
|
|
|
|
+ }
|
|
|
|
+ catch(IOException e) {
|
|
|
|
+ throw new RuntimeException(e); // Not likely to happen
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public String toString(String charsetName) {
|
|
|
|
+ try(ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
|
|
|
+ stream(baos);
|
|
|
|
+ return baos.toString(charsetName);
|
|
|
|
+ }
|
|
|
|
+ catch(IOException e) {
|
|
|
|
+ throw new RuntimeException(e); // Not likely to happen
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void stream(OutputStream os) {
|
|
|
|
+ ensurePreProcessed(true); //forcing at each rendering in case the underlying datas have changed
|
|
|
|
+
|
|
|
|
+ PrintStream ps = new PrintStream(os);
|
|
|
|
+ int columnCount = getColumnCount();
|
|
|
|
+
|
|
|
|
+ int borderlessWidth = 3 * (columnCount-1);
|
|
|
|
+ for(int colNum = 0; colNum < columnCount; colNum++) {
|
|
|
|
+ borderlessWidth += getRenderedWidth(colNum); // calls preProcess
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Top line
|
|
|
|
+ separator(ps, BORDER_TOP_BOTTOM, borderlessWidth);
|
|
|
|
+
|
|
|
|
+ // Headers
|
|
|
|
+ if(isHeaders()) {
|
|
|
|
+ List<String> paddedHeaderValues = new ArrayList<>(columnCount);
|
|
|
|
+
|
|
|
|
+ ps.append(BORDER_LEFT).append(BORDER_PADDING);
|
|
|
|
+ for (int colNum = 0; colNum < columnCount; colNum++) {
|
|
|
|
+ String renderedHeader = getRenderedHeader(colNum);
|
|
|
|
+ paddedHeaderValues.add(renderedHeader);
|
|
|
|
+ }
|
|
|
|
+ ps.append(String.join(BORDER_PADDING + COLUMN_SEPARATOR + BORDER_PADDING, paddedHeaderValues));
|
|
|
|
+ ps.append(BORDER_PADDING).println(BORDER_RIGHT);
|
|
|
|
+
|
|
|
|
+ // Separator
|
|
|
|
+ separator(ps, BORDER_TOP_BOTTOM, borderlessWidth);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ boolean canDrawLine = false;
|
|
|
|
+
|
|
|
|
+ // Table data
|
|
|
|
+ int rowCount = cellRenderCache.size();
|
|
|
|
+ for(int rowNum = 0; rowNum < rowCount; rowNum++) {
|
|
|
|
+ // Separator. It's easier to test like this than after an actual line.
|
|
|
|
+ if(canDrawLine) {
|
|
|
|
+ separator(ps, BORDER_LINE, borderlessWidth);
|
|
|
|
+ }
|
|
|
|
+ canDrawLine = true;
|
|
|
|
+
|
|
|
|
+ List<String> paddedRowValues = new ArrayList<>(columnCount);
|
|
|
|
+ ps.append(BORDER_LEFT).append(BORDER_PADDING);
|
|
|
|
+ for (int colNum = 0; colNum < columnCount; colNum++) {
|
|
|
|
+ String renderedCell = getRenderedCell(rowNum, colNum);
|
|
|
|
+ paddedRowValues.add(renderedCell);
|
|
|
|
+ }
|
|
|
|
+ ps.append(String.join(BORDER_PADDING + COLUMN_SEPARATOR + BORDER_PADDING, paddedRowValues));
|
|
|
|
+ ps.append(BORDER_PADDING).println(BORDER_RIGHT);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Bottom
|
|
|
|
+ separator(ps, BORDER_TOP_BOTTOM, borderlessWidth);
|
|
|
|
+
|
|
|
|
+ ps.flush();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ protected String pad(String txt, int length, Align align) {
|
|
|
|
+ char compChar = BORDER_PADDING;
|
|
|
|
+ switch(align) {
|
|
|
|
+ case RIGHT:
|
|
|
|
+ return padLeft(txt, compChar, length);
|
|
|
|
+
|
|
|
|
+ case CENTER:
|
|
|
|
+ return padCenter(txt, compChar, length);
|
|
|
|
+
|
|
|
|
+ case LEFT:
|
|
|
|
+ return padRight(txt, compChar, length);
|
|
|
|
+
|
|
|
|
+ default:
|
|
|
|
+ throw new UnsupportedOperationException(align.name());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ private void separator(PrintStream ps, char c, int borderlessWidth) {
|
|
|
|
+ ps.append(BORDER_LEFT).append(c);
|
|
|
|
+ for(int t = 0; t < borderlessWidth; t++) {
|
|
|
|
+ ps.append(c);
|
|
|
|
+ }
|
|
|
|
+ ps.append(c).println(BORDER_RIGHT);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ /**
|
|
|
|
+ * Renderes a textual representation of the header cell
|
|
|
|
+ * @param value
|
|
|
|
+ * @param colNum
|
|
|
|
+ * @return textual representation of the header cell. MUST NOT return null
|
|
|
|
+ */
|
|
|
|
+ private final String safeRenderHeader(Object value, int colNum) {
|
|
|
|
+ String result = renderHeader(value, colNum);
|
|
|
|
+ if(result == null) {
|
|
|
|
+ result = ToStringConsoleRenderer.instance.render(value, colNum);
|
|
|
|
+ }
|
|
|
|
+ return result;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected String renderHeader(Object value, int column) {
|
|
|
|
+ ConsoleHeaderRenderer headerRenderer = getHeaderRenderer(column);
|
|
|
|
+ if(headerRenderer == null) {
|
|
|
|
+ Class<?> headerClass = value != null ? value.getClass() : null;
|
|
|
|
+ headerRenderer = getDefaultHeaderRenderer(headerClass);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ String result = null;
|
|
|
|
+ if(headerRenderer != null) {
|
|
|
|
+ result = headerRenderer.render(value, column);
|
|
|
|
+ }
|
|
|
|
+ return result;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private final String safeRenderCell(Object value, int rowNum, int colNum) {
|
|
|
|
+ String result = renderCell(value, rowNum, colNum);
|
|
|
|
+ if(result == null) {
|
|
|
|
+ result = ToStringConsoleRenderer.instance.render(value, rowNum, colNum);
|
|
|
|
+ }
|
|
|
|
+ return result;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ protected String renderCell(Object value, int rowNum, int colNum) {
|
|
|
|
+ ConsoleCellRenderer cellRenderer;
|
|
|
|
+ if(NonExistent.isIt(value)) {
|
|
|
|
+ cellRenderer = getDefaultCellRenderer(NonExistent.class);
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ cellRenderer = getCellRenderer(rowNum, colNum);
|
|
|
|
+ if(cellRenderer == null) {
|
|
|
|
+ Class<?> columnClass = getColumnClass(colNum);
|
|
|
|
+ cellRenderer = getDefaultCellRenderer(columnClass);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ String result = null;
|
|
|
|
+ if(cellRenderer != null) {
|
|
|
|
+ result = cellRenderer.render(value, rowNum, colNum);
|
|
|
|
+ }
|
|
|
|
+ return result;
|
|
|
|
+ }
|
|
|
|
+}
|