|  | @@ -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;
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 |