001// Copyright (c) 2001 Hursh Jain (http://www.mollypages.org) 
002// The Molly framework is freely distributable under the terms of an
003// MIT-style license. For details, see the molly pages web site at:
004// http://www.mollypages.org/. Use, modify, have fun !
005
006package fc.io;
007
008import java.io.*;
009import java.util.*;
010import java.util.regex.*;
011
012import fc.util.*;
013
014/** 
015Prints a table formatted using plain text/ascii characters.
016Useful for console based output and/or formatted output to a
017file.
018
019@author hursh jain
020**/
021public class TablePrinter
022  {
023  private static final boolean dbg = false;
024  private static final Object Extra_Content_Marker = new Object();
025  
026  String      linesep = IOUtil.LINE_SEP;
027  PrintConfig   config;
028  PrintWriter   out;
029  int       columncount;  
030  String      rowborder;  
031  int       cellwidth;
032  String      spacing_str;
033  String      padding_str;
034  boolean     usingSpacing;   
035  boolean     printingHeader;  //printing a header line ?
036  int       current_linenum; //not including headers
037  int       current_cellnum;
038  String[]    wrappedcells;
039  int       wrapcells_left;
040
041  //use for max cell widths when using autofit
042  int[]     largest_width;
043  //used to cache table data when using autofit
044  //list of String[]
045  List      autoFitTableData = new ArrayList(); 
046  
047  int       currentAutoFitRow;
048  int       currentAutoFitCell;
049  
050  
051  /**
052  Constructs a new table printer.
053  
054  @param  columnCount   the number of columns in the table.
055              Attemps to print more than these
056              number of columns will result in 
057              a runtime exception.
058  @param  pw        the destination print stream
059  @param  config      the printing configuration
060  **/
061  public TablePrinter (int columnCount, PrintStream ps, PrintConfig config) 
062    {
063    this(columnCount, new PrintWriter(ps), config); 
064    }
065
066  /**
067  Constructs a new table printer.
068  
069  @param  columnCount   the number of columns in the table.
070              Attemps to print more than these
071              number of columns will result in 
072              a runtime exception.
073  @param  pw        the destination print writer
074  @param  config      the printing configuration
075  **/
076  public TablePrinter (int columnCount, PrintWriter pw, PrintConfig config) 
077    {
078    Argcheck.notnull(config, "config was null");
079    Argcheck.notnull(pw, "printwriter/stream was null");
080    this.columncount= columnCount;
081    this.out = pw;
082    this.config = config;
083    cellwidth = config.cellwidth; 
084    padding_str = StringUtil.repeatToWidth(
085      config.cellpadding_glyph, config.cellpadding);
086    
087    spacing_str = StringUtil.repeatToWidth(
088        config.cellspacing_glyph, config.cellspacing);
089
090    usingSpacing = config.cellspacing > 0;
091    printingHeader = false;
092    wrappedcells = new String[columncount];
093    initRowBorder();
094    }
095
096
097  /*******************************************************  
098
099  Layout rules:
100  upper and lower cell borders look like:
101  
102  +-----+------+-----+------+
103  |   |    |     |    |  
104  |   |    |     |    |
105  +-----+------+-----+------+
106
107  or:
108  
109  +-----+ +-----+ +-----+
110  |     | |   | |   |
111  |   | |   | |   |
112  +-----+ +-----+ +-----+
113
114      
115  We don't support cell specific borders and know the 
116  number of columns in the table beforehand.
117  
118  0) startTable
119    - prints the initial table header is applicable
120  
121  1) startRow - 
122    - initializes some variables
123    - if this is a new row, 
124      - writes spacing
125      - writes entire top border
126  
127  2) printcell 
128    - if spacing
129      - write spacing, left border, content, right border
130    - no spacing
131      - write left border, content    
132    - tracks whether row contents need to be wrapped
133    if wrapping needed, sets a flag ( we have to wait
134    until entire row is written before writing a new
135    row containing the wrapped contents )
136  
137  3) endRow
138    - if no spacing 
139      writes last most right border (not needed if we
140      are using spacing, since in that case each cell
141      writes it's right border)
142    - write spacing (if using spacing)
143    - if wrapping is needed, calls startRow again
144    specifying that this is *not* a new row (and
145    hence the new row border will not be written)
146    - if wrapping not needed, and using spacing, writes
147    the bottom border (needed to delimit rows with spacing)
148    - write header row again if applicable (if header rows are 
149    repeated every x number of lines)
150      
151  4) endTable
152    - write border and spacing after the last row (we don't know
153    beforehand the # of rows, so we wait until the last
154    row is written before writing the ending spacing)
155    
156  ********************************************************/
157      
158  /** This method should be called to start the table **/
159  public void startTable() 
160    {
161    if (config.autoFit) {
162      startTableAutoFit();
163      return;
164      }
165    
166    if (config.header != null) 
167      printHeader();
168    }
169  
170  /** This method should be called to finish the table **/
171  public void endTable() 
172    {
173    if (config.autoFit) {
174      endTableAutoFit();
175      return;
176      }
177  
178    if (! usingSpacing) {
179      printRowBorder();
180      }
181      
182    if (usingSpacing) {
183      printVerticalSpacing(); 
184      }
185      
186    //if prinstream->printwriter, then unless we flush,
187    //writes to underlying printstream may not appear
188    //as expected (and the sequence can be screwy if
189    //we are also writing to the underlying printstream
190    //independently
191    out.flush();
192    }
193  
194  /** This method should be called to start a new row **/
195  public void startRow() 
196    {
197    if (config.autoFit)  {
198      startRowAutoFit();
199      return;
200      }
201
202    current_cellnum = 0;
203      
204    //logical non header lines
205    if (! printingHeader && rowFinished()) 
206      current_linenum++;  //first line = 1
207    
208    if (rowFinished()) 
209      {
210      if (usingSpacing) {
211        printVerticalSpacing(); 
212        }
213      printRowBorder();
214      } 
215    }
216    
217  void endRowCommon() 
218    {
219    if (! usingSpacing) {
220      if (config.printborder) {
221        out.print(config.cellborder_v); //right most border
222        }
223      }
224    else  
225      out.print(spacing_str); //right most spacing
226    
227    out.print(linesep); 
228    } 
229  
230  /** This method should be called to finish the existing row **/
231  public void endRow() 
232    { 
233    if (config.autoFit)  {
234      endRowAutoFit();
235      return;
236      }
237            
238    endRowCommon();
239        
240    if (config.cellwrap) 
241      {
242      while (! rowFinished() ) 
243        {
244        startRow(); 
245        for (int n = 0; n < columncount; n++) {
246          printCell(wrappedcells[n]);
247          }
248        endRowCommon();   
249        }
250      }   
251  
252    if (usingSpacing) {
253      printRowBorder();   
254      }
255    
256    if (! printingHeader && repeatHeader()) {
257      printHeader();
258      }
259    }
260    
261  boolean repeatHeader() 
262    {
263    int pagesize = config.pageSize;
264    
265    if (pagesize <= 0 || ! config.headerEveryPage)
266      return false;
267    
268    if (current_linenum == 0)
269      return false;
270    
271    //System.out.println("current_linenum[" + current_linenum + "] % pagesize[" + pagesize + "] = " + (current_linenum % pagesize));
272  
273    if ((current_linenum % pagesize) == 0) {
274      return true;
275      }
276      
277    return false;
278    }
279
280  /** 
281  This method should be called to print a new cell in the
282  current row. This method should not be invoked for more
283  columns than the table was instantiated with, otherwise a
284  runtime exception will be thrown.
285  **/
286  public void printCell(String str) 
287    {   
288    if (current_cellnum == columncount)
289      throw new IllegalArgumentException("Cannot add more cells than number of columns in this table. (max columns=" + columncount + ")");
290    
291    str = removeEmbeddedSpaces(str);
292        
293    if (config.autoFit)  {
294      printCellAutoFit(str);
295      return;
296      }
297  
298    //left spacing
299    if (usingSpacing) {
300      out.print(spacing_str);
301      }
302        
303    //left border
304    if (config.printborder) {
305      out.print(config.cellborder_v); 
306      }
307
308    printCellContent(str);
309  
310    //right border if spaced
311    if (usingSpacing) {
312      if (config.printborder) {
313        out.print(config.cellborder_v); 
314        }
315      }   
316  
317    current_cellnum++;
318    }
319      
320  public String toString() {
321    return "TablePrinter. Config = " + config;
322    } 
323  
324  //== impl. methods ===================================
325          
326  void printCellContent(String str) 
327    {   
328    int width = config.getCellWidthForColumn(current_cellnum);
329
330    int strlen = (str == null) ? 0 : str.length();
331    
332    //align cell contents appropriately
333    String content = StringUtil.fixedWidth(str, width, config.align); 
334    
335    out.print(padding_str);
336    out.print(content);
337    out.print(padding_str);
338    
339    if (dbg) System.out.println("current cell=" + current_cellnum);
340  
341    if (config.cellwrap)
342      {
343      if (strlen > width) 
344        {
345        //don't double count wrapped cells
346        if (wrappedcells[current_cellnum] == null)
347          wrapcells_left++;
348
349        //this cell needs to be wrapped,save the extra portion
350        wrappedcells[current_cellnum] = str.substring(width, strlen); 
351        }
352      else {
353        if (wrappedcells[current_cellnum] != null)
354          wrapcells_left--;
355        wrappedcells[current_cellnum] = null;
356        }
357      }   
358    if (dbg) System.out.println("Wrapped cells left=" + wrapcells_left + "; Wrapped:" + Arrays.asList(wrappedcells));
359    }
360
361  boolean rowFinished() {
362    return wrapcells_left == 0;
363    }
364
365  void printHeader() 
366    {
367    if (config.header == null)
368      return;
369      
370    printingHeader = true;
371    startRow();
372    for (int n = 0; n < config.header.length; n++)
373      printCell(config.header[n]);
374    endRow();
375    printingHeader = false;
376    }
377  
378  void printRowBorder() 
379    {
380    if (! config.printborder)
381      return;
382      
383    out.print(rowborder); 
384    } 
385
386  private void initRowBorder() 
387    {
388    //new Exception().printStackTrace();
389    int default_padded_cellwidth = 
390        cellwidth + 2 * config.cellpadding;
391    
392    if (! config.printborder)
393      return;
394    
395    StringBuffer buf = new StringBuffer(
396        columncount * (1+default_padded_cellwidth+config.cellspacing));
397      
398    for (int n = 0; n < columncount; n++)
399      {
400      if (usingSpacing)
401        buf.append(spacing_str);
402      buf.append(config.cellcorner);
403      int padded_cellwidth = config.getCellWidthForColumn(n) 
404                  + 2 * config.cellpadding;
405      for (int k = 0; k < padded_cellwidth; k++) 
406        {
407        buf.append(config.cellborder_h);
408        }
409      if (usingSpacing)
410        buf.append(config.cellcorner);
411      }
412    if (! usingSpacing)
413      buf.append(config.cellcorner);
414    buf.append(linesep);
415    rowborder = buf.toString();
416    }
417
418void printVerticalSpacing() 
419  {
420  //1 lesser for aesthetics
421  for (int k = 1; k < config.cellspacing; k++) { 
422    out.print(linesep);
423    }
424  }     
425
426//== autofit methods ================
427
428void startTableAutoFit() {
429  largest_width = new int[columncount];
430  }
431  
432void endTableAutoFit() 
433  {
434  if (dbg) {
435    for (Iterator it = autoFitTableData.iterator(); it.hasNext(); /*empty*/) {
436      String[] item = (String[]) it.next();
437      System.out.println(Arrays.asList(item));
438      } 
439    }
440    
441  // == 2nd pass ================================== 
442  int rowcount = autoFitTableData.size();
443    if (rowcount == 0)
444      return;
445
446    //we are already handling autofit rendering, so terminate further recursion
447    config.setAutoFit(false); 
448
449    for (int n = 0; n < largest_width.length; n++) {
450    if (dbg) System.out.println("largest_width["+n+"]="+largest_width[n]);
451      config.setCellWidthForColumn(n, largest_width[n]);
452      }
453
454    //TablePrinter printer = new TablePrinter(columncount, out, config);
455  TablePrinter printer = this;
456  printer.initRowBorder();
457  
458    printer.startTable();
459    int n = 0;
460    while (n < rowcount) 
461      {
462      printer.startRow();
463      for (int k = 0; k < columncount; k++) {
464        printer.printCell(
465        ((String[])autoFitTableData.get(n))[k] );
466        }
467      printer.endRow();
468      n++;
469      }
470    
471    printer.endTable(); 
472  }
473  
474void startRowAutoFit() {
475  currentAutoFitCell = 0;
476  autoFitTableData.add(currentAutoFitRow, new String[columncount]);
477  }
478  
479void endRowAutoFit() {
480  currentAutoFitRow++;
481  }
482
483void printCellAutoFit(String str) 
484  {
485  int len = (str != null) ? str.length() : "null".length();
486  
487  if (largest_width[currentAutoFitCell] < len)
488     largest_width[currentAutoFitCell] = len;
489
490  String[] row = (String[]) autoFitTableData.get(currentAutoFitRow);
491  row[currentAutoFitCell++] = str;  
492  }
493
494final private Pattern newlines = Pattern.compile("\\r|\\n|\\r\\n");
495String removeEmbeddedSpaces(String str)
496  {
497  /*
498  we convert embedded newlines in the string to spaces
499  otherwise the embedded newline will cause printing
500  to start at the left most margin. i.e.,
501  
502  baz\n\baz2 will print like
503      
504  A    B    C
505  x  y    baz
506  baz2
507  
508  but we want
509        <---> 
510  A    B      C
511  x  y    baz
512        baz2
513  
514  */
515
516  if (str != null)
517    {
518    str = newlines.matcher(str).replaceAll(" ");
519    }
520  return str;
521  }
522
523/** 
524Configuration object containing for table printing object.
525
526Some methods in this class return <tt>this</tt> object for method
527chaining convenience. 
528<p>
529Note: Alas ! TablePrinter does not support cellspans across
530columns or rows. That would make things too complicated for
531this implementation.
532**/
533public static class PrintConfig
534  {
535  static final ToString.Style style = new ToString.Style();
536  static {
537    style.reflectVisibleLevel = ToString.Style.VisibleLevel.PRIVATE;
538    }
539      
540  private int    cellwidth       =  20;
541  
542  //individual widths if specified
543  private Map    cellwidthMap    = new HashMap();
544   
545  private String   cellcorner      =  "+";
546  private String   cellborder_h      =  "-";
547  private String   cellborder_v      =  "|";
548  private String   cellpadding_glyph   =  " ";
549  private String   cellspacing_glyph   =  " ";
550  private boolean  cellwrap      =  true;
551  private boolean  autoFit       =  false;
552  private boolean  printborder       =  true;
553  private int    cellspacing       =  0;
554  private int    cellpadding       =  0;
555  private HAlign   align         =  HAlign.LEFT;
556  private int    pageSize      = -1;    
557  private boolean  headerEveryPage   = true;
558  private String[] header;
559  
560  /** 
561  Constructs a new PrintConfig with the following default options. 
562  <ul>
563    <li>cell corner: the <tt>+</tt> character 
564    <li>horizontal cell border: the <tt>-</tt> character 
565    <li>vertical cell border, the <tt>|</tt> character 
566    <li>width of each column: 20 chars 
567    <li>horizontal alignment of each cell: {@link HAlign#LEFT}
568    <li>The character used for cellpadding and cellspacing is a
569    blank space.
570  </ul>
571  **/
572  public PrintConfig() {
573    }
574  
575  /** 
576  Optionally sets the header row for the table **/
577  public void setHeader(String[] header) {
578    this.header = header;
579    } 
580
581  /** 
582  Gets the header row for the table if set. If not
583  set, return null
584  **/
585  public String[] getHeader() {
586    return this.header;
587    } 
588
589  
590  /** 
591  Sets the number of lines on each page. A page is a
592  logical unit that typically shows some number of lines
593  on the screen without the need to scroll the page. This
594  value is useful in conjunction with other page specific
595  settings like {@link showPageHeading()}
596  
597  @param  lines number of lines on the page, specify a zero or
598          negative quantity for a single page. (number of
599          lines <b>not</b> including lines occupied by the table 
600          header itself, if the header is printed).
601  **/
602  public PrintConfig setPageSize(int lines) {
603    this.pageSize = lines;
604    return this;
605    }
606    
607  /** 
608  Specifies that page heading (if set) on each separate
609  page. This may be ignored if no heading has been set.
610  <tt>true</tt> by default, although headers will still
611  not be printed until the {@link setPageSize} method is
612  invoked.
613  **/
614  public PrintConfig headerEveryPage(boolean show) {
615    this.headerEveryPage = show;
616    return this;
617    }
618  
619  /** Sets the alignment of each cell. By default: 
620  <tt>{@link HAlign#LEFT left}</tt>
621  **/
622  public PrintConfig setAlign(HAlign align) {
623    this.align = align;
624    return this;
625    }
626
627  /** 
628  Sets the string (typically a single character) that makes up a 
629  cell corner. Defaults to <tt>+</tt>
630  **/
631  public PrintConfig setCellCorner(String str) {
632    this.cellcorner = str;
633    return this;
634    }
635
636  /** 
637  Sets the string (typically a single character) that makes up a 
638  horizontal cell border. Defaults to <tt>-</tt>
639  **/
640  public PrintConfig setCellBorderHorizontal(String str) {
641    this.cellborder_h = str;
642    return this;
643    }
644
645  /** 
646  Sets the string (typically a single character) that makes up a 
647  vertical cell border. Defaults to <tt>|</tt>
648  **/
649  public PrintConfig setCellBorderVertical(String str) {
650    this.cellborder_v = str;
651    return this;
652    }
653
654  /** 
655  Sets the string (typically a single character) that makes up
656  the cellpadding. Defaults to <tt>" "</tt>
657  **/
658  public PrintConfig setCellPaddingGlyph(String str) {
659    this.cellpadding_glyph = str;
660    return this;
661    }
662
663  /** 
664  Sets the string (typically a single character) that makes up
665  the cellspacing. Defaults to <tt>" "</tt>
666  **/
667  public PrintConfig setCellSpacingGlyph(String str) {
668    this.cellspacing_glyph = str;
669    return this;
670    }
671
672  /** 
673  Sets the width of each cell. Defaults to <tt>20</tt>. 
674  This width is common to all cells.
675
676  @param  width the width of each cell
677  **/
678  public PrintConfig setCellWidth(int width) {
679    if (width <= 0) {
680      throw new IllegalArgumentException("cell width must be greater than zero");
681      }
682    this.cellwidth = width;
683    return this;
684    } 
685
686  /** 
687  Sets the cell width of the the specified column. cells
688  are numbered starting from 0 
689  
690  @param  column  the cell number
691  @param  width the desired character width
692  **/
693  public PrintConfig setCellWidthForColumn(int column, int width) {
694    cellwidthMap.put(Integer.valueOf(column), Integer.valueOf(width));
695    return this;
696    }
697  
698  /** 
699  convenience method for internal use:
700  returns the cellwidth if set otherwise the default cellwidth
701  **/
702  int getCellWidthForColumn(int column) 
703    {
704    Object obj = cellwidthMap.get(Integer.valueOf(column));
705    if (obj != null) {
706      return ((Integer) obj).intValue();
707      }
708    return cellwidth;
709    }
710  
711  
712  /** 
713  Cell wrapping is on by default and the contents of any column that 
714  exceed the width are wrapped within each cell (using the current platforms line 
715  seperator for newlines within the cell). 
716    
717  @param  wrap <tt>true</tt>  To turn cell wrapping on, <tt>false</tt> 
718                to turn cell wrapping off and show
719                fixed width contents only.
720  **/ 
721  public PrintConfig setCellWrap(boolean wrap) {
722    cellwrap = wrap;  
723    return this;
724    }
725  
726  /** 
727  Sets each cell to expand to the size needed for the maximum
728  sized cell in that column. By default, this is <tt>false</tt>.
729  Setting this to <tt>true</tt> automatically turns <b>off</b> 
730  cell wrapping.
731  <p>
732  Note: using autofit may result in slower performance
733  because the table almost always has to be rendered in
734  two passes.
735  
736  @param  autofit   <tt>true</tt> to turn autofit on, 
737            <tt>false</tt> otherwise.
738  **/
739  public PrintConfig setAutoFit(boolean autofit) 
740    {
741    autoFit = autofit;
742    if (autofit)
743      cellwrap = false;
744    return this;
745    } 
746  
747  /**  
748  Specifies whether table and cell borders are printed. By
749  default this is true.
750
751  @param  print <tt>true</tt> to print borders, <tt>false</tt> otherwise
752  **/
753  public PrintConfig setPrintBorders(boolean print) {
754    printborder = print;
755    return this;
756    }
757  
758  /**  
759  Specifies the cell spacing between cells. This is useful
760  for tables with no borders. By default, this value is
761  zero.
762  **/
763  public PrintConfig setCellSpacing(int width) {
764    cellspacing = width;
765    return this;
766    }
767    
768  /**
769  Specifies the cell padding for each cell. This is useful for
770  tables with no borders. By default, this value is zero.
771  **/
772  public PrintConfig setCellPadding(int width) {
773    cellpadding = width;
774    return this;
775    }
776  
777  /** 
778  Prints a short description of this object. 
779  **/
780  public String toString() 
781    { 
782    return new ToString(this, style).reflect().render();
783    }
784    
785  } //~PrintConfig
786
787public static void main(String[] args)
788  { 
789  System.out.println("Table with borders and *NO* wrapping, (truncated to fixed cell width)");
790  //borders
791  PrintConfig config = new PrintConfig();
792  config.setCellWrap(false);
793  printTest(config, null);
794
795
796  System.out.println("Table with borders and autofit");
797  //borders
798  config = new PrintConfig();
799  config.setAutoFit(true);
800  printTest(config, null);
801
802
803  System.out.println("Table with borders and wrapping");
804  //borders
805  config = new PrintConfig();
806  printTest(config, null);
807  
808  System.out.println(""); 
809  System.out.println("Table with borders and padding & spacing");
810  //borders + padding
811  config = new PrintConfig();
812  config.setAutoFit(true);
813  config.setCellSpacing(2);
814  config.setCellPadding(1);
815  config.setCellWidth(5);
816  printTest(config, null);
817
818  System.out.println(""); 
819  System.out.println("Table with borders and padding & spacing and AUTOFIT");
820  //borders + padding
821  config = new PrintConfig();
822  config.setCellSpacing(2);
823  config.setCellPadding(1);
824  config.setCellWidth(5);
825  printTest(config, null);
826
827  System.out.println(""); 
828  System.out.println("Table with borders, spacing=2 page size = 2 lines and headers = true");
829  config = new PrintConfig();
830  config.setCellSpacing(2);
831  config.setCellWidth(5);
832  config.setPageSize(2);
833  config.headerEveryPage(true);
834  printTest(config, new String[] {"Header", "A", "helloworld", "d", "End Header"});
835
836  System.out.println(""); 
837  System.out.println("Table with borders, no padding and default alignment of CENTER");
838  //borders + padding
839  config = new PrintConfig();
840  config.setCellSpacing(0);
841  config.setCellPadding(0);
842  config.setCellWidth(15);
843  config.setAlign(HAlign.CENTER);
844  printTest(config, null);
845
846  System.out.println("");
847  System.out.println("Table with no borders and padding & spacing");
848  //no borders + padding
849  config = new PrintConfig();
850  config.setPrintBorders(false);
851  config.setCellWidth(5);
852  config.setCellSpacing(2);
853  config.setCellPadding(3);
854  printTest(config, null);
855
856  }
857
858
859static void printTest(PrintConfig config, String[] header) 
860  {
861  TablePrinter printer = new TablePrinter(5, System.out, config);
862
863  if (header != null)
864    config.setHeader(header);
865    
866  printer.startTable();
867
868  //row 1
869  printer.startRow();
870  printer.printCell("abcdef");
871  printer.printCell("4324");
872  printer.printCell("Q");
873  printer.printCell("");
874  printer.printCell("abc");
875  printer.endRow();
876    
877  //2
878  printer.startRow();
879  printer.printCell("row2: abc");
880  printer.printCell("the quick brown \n-fox jumps over the lazy dog");
881  printer.printCell("hello");
882  printer.printCell("world");
883  printer.printCell(null);
884  printer.endRow();
885
886
887  //3
888  printer.startRow();
889  printer.printCell("row3");
890  printer.printCell("a");
891  printer.printCell(null);
892  printer.printCell(null);
893  printer.printCell(null);
894  printer.endRow();
895  
896  printer.endTable();
897  
898  System.out.println("TablePrinter.toString()=" + printer);
899  System.out.println("");
900  }
901  
902} //~TablePrinter