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
006 package fc.io;
007
008 import java.io.*;
009 import java.util.*;
010 import java.util.regex.*;
011
012 import fc.util.*;
013
014 /**
015 Prints a table formatted using plain text/ascii characters.
016 Useful for console based output and/or formatted output to a
017 file.
018
019 @author hursh jain
020 **/
021 public 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
418 void 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
428 void startTableAutoFit() {
429 largest_width = new int[columncount];
430 }
431
432 void 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
475 void startRowAutoFit() {
476 currentAutoFitCell = 0;
477 autoFitTableData.add(currentAutoFitRow, new String[columncount]);
478 }
479
480 void endRowAutoFit() {
481 currentAutoFitRow++;
482 }
483
484 void 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
495 final private Pattern newlines = Pattern.compile("\\r|\\n|\\r\\n");
496 String 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 /**
525 Configuration object containing for table printing object.
526
527 Some methods in this class return <tt>this</tt> object for method
528 chaining convenience.
529 <p>
530 Note: Alas ! TablePrinter does not support cellspans across
531 columns or rows. That would make things too complicated for
532 this implementation.
533 **/
534 public 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
788 public 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
860 static 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