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
447    //further recursion
448    config.setAutoFit(false); 
449
450    for (int n = 0; n < largest_width.length; n++) {
451    if (dbg) System.out.println("largest_width["+n+"]="+largest_width[n]);
452      config.setCellWidthForColumn(n, largest_width[n]);
453      }
454
455    //TablePrinter printer = new TablePrinter(columncount, out, config);
456  TablePrinter printer = this;
457  printer.initRowBorder();
458  
459    printer.startTable();
460    int n = 0;
461    while (n < rowcount) 
462      {
463      printer.startRow();
464      for (int k = 0; k < columncount; k++) {
465        printer.printCell(
466        ((String[])autoFitTableData.get(n))[k] );
467        }
468      printer.endRow();
469      n++;
470      }
471    
472    printer.endTable(); 
473  }
474  
475void startRowAutoFit() {
476  currentAutoFitCell = 0;
477  autoFitTableData.add(currentAutoFitRow, new String[columncount]);
478  }
479  
480void endRowAutoFit() {
481  currentAutoFitRow++;
482  }
483
484void printCellAutoFit(String str) 
485  {
486  int len = (str != null) ? str.length() : "null".length();
487  
488  if (largest_width[currentAutoFitCell] < len)
489     largest_width[currentAutoFitCell] = len;
490
491  String[] row = (String[]) autoFitTableData.get(currentAutoFitRow);
492  row[currentAutoFitCell++] = str;  
493  }
494
495final private Pattern newlines = Pattern.compile("\\r|\\n|\\r\\n");
496String removeEmbeddedSpaces(String str)
497  {
498  /*
499  we convert embedded newlines in the string to spaces
500  otherwise the embedded newline will cause printing
501  to start at the left most margin. i.e.,
502  
503  baz\n\baz2 will print like
504      
505  A    B    C
506  x  y    baz
507  baz2
508  
509  but we want
510        <---> 
511  A    B      C
512  x  y    baz
513        baz2
514  
515  */
516
517  if (str != null)
518    {
519    str = newlines.matcher(str).replaceAll(" ");
520    }
521  return str;
522  }
523
524/** 
525Configuration object containing for table printing object.
526
527Some methods in this class return <tt>this</tt> object for method
528chaining convenience. 
529<p>
530Note: Alas ! TablePrinter does not support cellspans across
531columns or rows. That would make things too complicated for
532this implementation.
533**/
534public static class PrintConfig
535  {
536  static final ToString.Style style = new ToString.Style();
537  static {
538    style.reflectVisibleLevel = ToString.Style.VisibleLevel.PRIVATE;
539    }
540      
541  private int    cellwidth       =  20;
542  
543  //individual widths if specified
544  private Map    cellwidthMap    = new HashMap();
545   
546  private String   cellcorner      =  "+";
547  private String   cellborder_h      =  "-";
548  private String   cellborder_v      =  "|";
549  private String   cellpadding_glyph   =  " ";
550  private String   cellspacing_glyph   =  " ";
551  private boolean  cellwrap      =  true;
552  private boolean  autoFit       =  false;
553  private boolean  printborder       =  true;
554  private int    cellspacing       =  0;
555  private int    cellpadding       =  0;
556  private HAlign   align         =  HAlign.LEFT;
557  private int    pageSize      = -1;    
558  private boolean  headerEveryPage   = true;
559  private String[] header;
560  
561  /** 
562  Constructs a new PrintConfig with the following default options. 
563  <ul>
564    <li>cell corner: the <tt>+</tt> character 
565    <li>horizontal cell border: the <tt>-</tt> character 
566    <li>vertical cell border, the <tt>|</tt> character 
567    <li>width of each column: 20 chars 
568    <li>horizontal alignment of each cell: {@link HAlign#LEFT}
569    <li>The character used for cellpadding and cellspacing is a
570    blank space.
571  </ul>
572  **/
573  public PrintConfig() {
574    }
575  
576  /** 
577  Optionally sets the header row for the table **/
578  public void setHeader(String[] header) {
579    this.header = header;
580    } 
581
582  /** 
583  Gets the header row for the table if set. If not
584  set, return null
585  **/
586  public String[] getHeader() {
587    return this.header;
588    } 
589
590  
591  /** 
592  Sets the number of lines on each page. A page is a
593  logical unit that typically shows some number of lines
594  on the screen without the need to scroll the page. This
595  value is useful in conjunction with other page specific
596  settings like {@link showPageHeading()}
597  
598  @param  lines number of lines on the page, specify a zero or
599          negative quantity for a single page. (number of
600          lines <b>not</b> including lines occupied by the table 
601          header itself, if the header is printed).
602  **/
603  public PrintConfig setPageSize(int lines) {
604    this.pageSize = lines;
605    return this;
606    }
607    
608  /** 
609  Specifies that page heading (if set) on each separate
610  page. This may be ignored if no heading has been set.
611  <tt>true</tt> by default, although headers will still
612  not be printed until the {@link setPageSize} method is
613  invoked.
614  **/
615  public PrintConfig headerEveryPage(boolean show) {
616    this.headerEveryPage = show;
617    return this;
618    }
619  
620  /** Sets the alignment of each cell. By default: 
621  <tt>{@link HAlign#LEFT left}</tt>
622  **/
623  public PrintConfig setAlign(HAlign align) {
624    this.align = align;
625    return this;
626    }
627
628  /** 
629  Sets the string (typically a single character) that makes up a 
630  cell corner. Defaults to <tt>+</tt>
631  **/
632  public PrintConfig setCellCorner(String str) {
633    this.cellcorner = str;
634    return this;
635    }
636
637  /** 
638  Sets the string (typically a single character) that makes up a 
639  horizontal cell border. Defaults to <tt>-</tt>
640  **/
641  public PrintConfig setCellBorderHorizontal(String str) {
642    this.cellborder_h = str;
643    return this;
644    }
645
646  /** 
647  Sets the string (typically a single character) that makes up a 
648  vertical cell border. Defaults to <tt>|</tt>
649  **/
650  public PrintConfig setCellBorderVertical(String str) {
651    this.cellborder_v = str;
652    return this;
653    }
654
655  /** 
656  Sets the string (typically a single character) that makes up
657  the cellpadding. Defaults to <tt>" "</tt>
658  **/
659  public PrintConfig setCellPaddingGlyph(String str) {
660    this.cellpadding_glyph = str;
661    return this;
662    }
663
664  /** 
665  Sets the string (typically a single character) that makes up
666  the cellspacing. Defaults to <tt>" "</tt>
667  **/
668  public PrintConfig setCellSpacingGlyph(String str) {
669    this.cellspacing_glyph = str;
670    return this;
671    }
672
673  /** 
674  Sets the width of each cell. Defaults to <tt>20</tt>. 
675  This width is common to all cells.
676
677  @param  width the width of each cell
678  **/
679  public PrintConfig setCellWidth(int width) {
680    if (width <= 0) {
681      throw new IllegalArgumentException("cell width must be greater than zero");
682      }
683    this.cellwidth = width;
684    return this;
685    } 
686
687  /** 
688  Sets the cell width of the the specified column. cells
689  are numbered starting from 0 
690  
691  @param  column  the cell number
692  @param  width the desired character width
693  **/
694  public PrintConfig setCellWidthForColumn(int column, int width) {
695    cellwidthMap.put(new Integer(column), new Integer(width));
696    return this;
697    }
698  
699  /** 
700  convenience method for internal use:
701  returns the cellwidth if set otherwise the default cellwidth
702  **/
703  int getCellWidthForColumn(int column) 
704    {
705    Object obj = cellwidthMap.get(new Integer(column));
706    if (obj != null) {
707      return ((Integer) obj).intValue();
708      }
709    return cellwidth;
710    }
711  
712  
713  /** 
714  Cell wrapping is on by default and the contents of any column that 
715  exceed the width are wrapped within each cell (using the current platforms line 
716  seperator for newlines within the cell). 
717    
718  @param  wrap <tt>true</tt>  To turn cell wrapping on, <tt>false</tt> 
719                to turn cell wrapping off and show
720                fixed width contents only.
721  **/ 
722  public PrintConfig setCellWrap(boolean wrap) {
723    cellwrap = wrap;  
724    return this;
725    }
726  
727  /** 
728  Sets each cell to expand to the size needed for the maximum
729  sized cell in that column. By default, this is <tt>false</tt>.
730  Setting this to <tt>true</tt> automatically turns <b>off</b> 
731  cell wrapping.
732  <p>
733  Note: using autofit may result in slower performance
734  because the table almost always has to be rendered in
735  two passes.
736  
737  @param  autofit   <tt>true</tt> to turn autofit on, 
738            <tt>false</tt> otherwise.
739  **/
740  public PrintConfig setAutoFit(boolean autofit) 
741    {
742    autoFit = autofit;
743    if (autofit)
744      cellwrap = false;
745    return this;
746    } 
747  
748  /**  
749  Specifies whether table and cell borders are printed. By
750  default this is true.
751
752  @param  print <tt>true</tt> to print borders, <tt>false</tt> otherwise
753  **/
754  public PrintConfig setPrintBorders(boolean print) {
755    printborder = print;
756    return this;
757    }
758  
759  /**  
760  Specifies the cell spacing between cells. This is useful
761  for tables with no borders. By default, this value is
762  zero.
763  **/
764  public PrintConfig setCellSpacing(int width) {
765    cellspacing = width;
766    return this;
767    }
768    
769  /**
770  Specifies the cell padding for each cell. This is useful for
771  tables with no borders. By default, this value is zero.
772  **/
773  public PrintConfig setCellPadding(int width) {
774    cellpadding = width;
775    return this;
776    }
777  
778  /** 
779  Prints a short description of this object. 
780  **/
781  public String toString() 
782    { 
783    return new ToString(this, style).reflect().render();
784    }
785    
786  } //~PrintConfig
787
788public static void main(String[] args)
789  { 
790  System.out.println("Table with borders and *NO* wrapping, (truncated to fixed cell width)");
791  //borders
792  PrintConfig config = new PrintConfig();
793  config.setCellWrap(false);
794  printTest(config, null);
795
796
797  System.out.println("Table with borders and autofit");
798  //borders
799  config = new PrintConfig();
800  config.setAutoFit(true);
801  printTest(config, null);
802
803
804  System.out.println("Table with borders and wrapping");
805  //borders
806  config = new PrintConfig();
807  printTest(config, null);
808  
809  System.out.println(""); 
810  System.out.println("Table with borders and padding & spacing");
811  //borders + padding
812  config = new PrintConfig();
813  config.setAutoFit(true);
814  config.setCellSpacing(2);
815  config.setCellPadding(1);
816  config.setCellWidth(5);
817  printTest(config, null);
818
819  System.out.println(""); 
820  System.out.println("Table with borders and padding & spacing and AUTOFIT");
821  //borders + padding
822  config = new PrintConfig();
823  config.setCellSpacing(2);
824  config.setCellPadding(1);
825  config.setCellWidth(5);
826  printTest(config, null);
827
828  System.out.println(""); 
829  System.out.println("Table with borders, spacing=2 page size = 2 lines and headers = true");
830  config = new PrintConfig();
831  config.setCellSpacing(2);
832  config.setCellWidth(5);
833  config.setPageSize(2);
834  config.headerEveryPage(true);
835  printTest(config, new String[] {"Header", "A", "helloworld", "d", "End Header"});
836
837  System.out.println(""); 
838  System.out.println("Table with borders, no padding and default alignment of CENTER");
839  //borders + padding
840  config = new PrintConfig();
841  config.setCellSpacing(0);
842  config.setCellPadding(0);
843  config.setCellWidth(15);
844  config.setAlign(HAlign.CENTER);
845  printTest(config, null);
846
847  System.out.println("");
848  System.out.println("Table with no borders and padding & spacing");
849  //no borders + padding
850  config = new PrintConfig();
851  config.setPrintBorders(false);
852  config.setCellWidth(5);
853  config.setCellSpacing(2);
854  config.setCellPadding(3);
855  printTest(config, null);
856
857  }
858
859
860static void printTest(PrintConfig config, String[] header) 
861  {
862  TablePrinter printer = new TablePrinter(5, System.out, config);
863
864  if (header != null)
865    config.setHeader(header);
866    
867  printer.startTable();
868
869  //row 1
870  printer.startRow();
871  printer.printCell("abcdef");
872  printer.printCell("4324");
873  printer.printCell("Q");
874  printer.printCell("");
875  printer.printCell("abc");
876  printer.endRow();
877    
878  //2
879  printer.startRow();
880  printer.printCell("row2: abc");
881  printer.printCell("the quick brown \n-fox jumps over the lazy dog");
882  printer.printCell("hello");
883  printer.printCell("world");
884  printer.printCell(null);
885  printer.endRow();
886
887
888  //3
889  printer.startRow();
890  printer.printCell("row3");
891  printer.printCell("a");
892  printer.printCell(null);
893  printer.printCell(null);
894  printer.printCell(null);
895  printer.endRow();
896  
897  printer.endTable();
898  
899  System.out.println("TablePrinter.toString()=" + printer);
900  System.out.println("");
901  }
902  
903} //~TablePrinter