.
.
java.sun.com developers.sun.com
.
   View this issue as simple text March 8, 2005    

In this Issue

Welcome to the Core Java Technologies Tech Tips for March 8, 2005. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE).

This issue covers:

-Printing JTables
-From StringTokenizer to Scanner

These tips were developed using the Java 2 Platform Standard Edition Development Kit 5.0 (JDK 5.0). You can download JDK 5.0 at http://java.sun.com/j2se/1.5.0/download.jsp.

This issue of the Core Java Technologies Tech Tips is written by Daniel H. Steinberg, Director of Java Offerings for Dim Sum Thinking, Inc, and editor-in-chief for java.net.

You can view this issue of the Tech Tips on the Web at http://java.sun.com/developer/JDCTechTips/2005/tt0308.html.

See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms.

For more Java technology content, visit these sites:

java.sun.com - The Java technology source for developers. Get the latest Java platform releases, tutorials, newsletters and more.

java.net - A web forum where enthusiasts of Java technology can collaborate and build solutions together.

java.com - The ultimate marketplace promoting Java technology, applications and services.

.
.

PRINTING JTABLES

Before J2SE 5.0, printing a simple JTable was challenging. J2SE 5.0 adds print() methods to the API for printing tables, adding headers and footers to the output, and specifying whether or not you want to fit the table on the printed page. This tip shows you how to use these new facilities to print a simple JTable. It also shows you how to customize the table by adding shading to alternate rows, and print the table with and without the shading.

Let's begin by refactoring the SimpleTableDemo program shown in the Swing tutorial. The refactoring collects the code that is common to all the examples in this tip. The data for the table is contained in the private static final variables named columnNames and data (these appear at the end of each code listing). The other common code is contained in the methods setUpJFrame() and getTablePanel(). These methods are used to set up the JFrame and JPanel that are used to display the JTable. (The common code is shown in the code for the following example, but is not repeated in subsequent examples.)

Here is the BaseTable class:

   import javax.swing.JTable;
   import javax.swing.SwingUtilities;
   import javax.swing.JFrame;
   import javax.swing.JPanel;
   import javax.swing.JScrollPane;
   import java.awt.Dimension;
   import java.awt.GridLayout;
   
   public class BaseTable extends JTable {
   
         public BaseTable() {
           super(data, columnNames);
           setUpJFrame();
         }
   
         public static void main(String[] args) {
           SwingUtilities.invokeLater(new Runnable() {
             public void run() {
               new BaseTable();
             }
           });
         }
   
         // Table Frame common to all examples in this tip
   
          private void setUpJFrame() {
          JFrame frame = new JFrame();
           frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
           frame.add( getTablePanel());
           frame.pack();
           frame.setVisible(true);
         }
   
        private JPanel getTablePanel(){
           JPanel jPanel = new JPanel(new GridLayout(1,0));
           jPanel.setOpaque(true);
          setPreferredScrollableViewportSize(
                                 new Dimension(500, 70));
           jPanel.add(new JScrollPane(this));
           return jPanel;
         }
   
         // Table Data common to all examples in this tip
   
         private static final String[] columnNames =
           {"First Name", "Last Name", "Sport",
            "# of Years", "Vegetarian"};
   
         private static final Object[][] data = {
           {"Mary", "Campione", "Snowboarding",
            new Integer(5), new Boolean(false)},
           {"Alison", "Huml", "Rowing",
            new Integer(3), new Boolean(true)},
           {"Kathy", "Walrath", "Knitting",
            new Integer(2), new Boolean(false)},
           {"Sharon", "Zakhour", "Speed reading",
            new Integer(20), new Boolean(true)},
           {"Philip", "Milne", "Pool",
            new Integer(10), new Boolean(false)}
         };
       }

BaseTable uses the recommended approach of creating and showing the GUI on the event dispatch thread, through invokeLater(). Compile and run BaseTable. You should see a five-column table that displays the data supplied by data.

BaseTable

It takes surprisingly little to shade alternate rows. You can do that by overriding the javax.swing.JTable prepareRenderer() method. First, obtain a handle to the current cell being rendered from the prepareRenderer() method you are overriding. Next, determine if the rowIndex is even or odd. In odd rows change the background color, and in even rows set the background to its current color. This is illustrated in the following ColorTable class:

   import javax.swing.JTable;
   import javax.swing.SwingUtilities;
   import javax.swing.JFrame;
   import javax.swing.JPanel;
   import javax.swing.JScrollPane;
   import javax.swing.table.TableCellRenderer;
   import java.awt.Dimension;
   import java.awt.GridLayout;
   import java.awt.Component;
   import java.awt.Color;
   
   public class ColorTable extends JTable {
   
         public ColorTable() {
           super(data, columnNames);
           setUpJFrame();
         }
   
      public Component prepareRenderer(
                                TableCellRenderer renderer,
                                int row, int col) {
           Component c = super.prepareRenderer(renderer,
                                      row, col);
           if (row % 2 == 0 && !isCellSelected(row,col)) {
             c.setBackground(Color.LIGHT_GRAY);
           } else {
             c.setBackground(getBackground());
           }
           return c;
         }
   
         public static void main(String[] args) {
           SwingUtilities.invokeLater(new Runnable() {
             public void run() {
               new ColorTable();
             }
           });
         }
         // put common code here . . .
     }

Compile and run ColorTable. You should see the previous table displayed with the odd rows striped.

ColorTable

Now let's print the undecorated table displayed by BaseTable. To do this, you need to call the print method that has been added to JTable. In this example, print() is called from the constructor. The no argument version of the print() method prints the JTable with no header or footer and selects FIT_WIDTH print mode (as opposed to NORMAL). You need to handle a possible PrinterException. The sample handles the possible exception by printing a stack trace.

   import javax.swing.JTable;
   import javax.swing.SwingUtilities;
   import javax.swing.JFrame;
   import javax.swing.JPanel;
   import javax.swing.JScrollPane;
   import java.awt.Dimension;
   import java.awt.GridLayout;
   import java.awt.print.PrinterException;
   
   public class BasePrintableTable extends JTable {
   
         public BasePrintableTable() {
           super(data, columnNames);
           setUpJFrame();
           try {
             print();
           } catch (PrinterException e) {
             e.printStackTrace();
           }
         }
   
         public static void main(String[] args) {
           SwingUtilities.invokeLater(new Runnable() {
             public void run() {
               new BasePrintableTable();
             }
           });
         }
         // put common code here . . .
  } 

Compile and run BasePrintableTable. You will see a table (although it will take a bit longer than the previous examples to display). You will then see a print dialog.

printing

Select a printer and click the Print button. The table should be sent to the printer you selected.

One way to enhance your table is to add a header and a footer to the printout. To do this, you need to use a version of print() that accepts parameters for entering the header and footer as MessageFormat objects. These objects are designed to produce centered text at the top and bottom of the page. You can also insert the page number by referring to the data that is provided in the first position. Here are the changes to the constructor needed to add the header and footer to the printout of the JTable:

   public BasePrintableTable() {
     super(data, columnNames);
     setUpJFrame();
     try {
           print(PrintMode.NORMAL, new MessageFormat(
             "Personal Info"),
           new MessageFormat("Page {0,number}"));
         } catch (PrinterException e) {
           e.printStackTrace();
         }
    } 

The header will be bold faced and centered. It will contain the text "Personal Info". The Footer will consist of the centered text "Page 1". In a multipage document, {0, number} would be replaced by the current page number formatted as a number.

footer

If you need tighter control over the output, you can use a signature of print() that allows you to specify whether the print dialog is displayed, and whether the modal dialog is shown during the printing operation. It also allows you to pass in a PrintRequestAttributeSet that specifies attributes to be used in printing.

Consider how you might combine the printing capabilities with the version of ColorTable that shades alternate rows of the JTable. A simple way to do this it to include the extensions from both the BasePrintableTable and the ColorTable at the same time, as follows:

   import javax.swing.JTable;
   import javax.swing.SwingUtilities;
   import javax.swing.JFrame;
   import javax.swing.JPanel;
   import javax.swing.JScrollPane;
   import javax.swing.table.TableCellRenderer;
   import java.awt.Dimension;
   import java.awt.GridLayout;
   import java.awt.Component;
   import java.awt.Color;
   import java.awt.print.PrinterException;
   
   public class PrintableColorTable extends JTable {
   
         public PrintableColorTable() {
           super(data, columnNames);
           setUpJFrame();
           try {
             print();
           } catch (PrinterException e) {
             e.printStackTrace();
           }
         }
   
      public Component prepareRenderer(
                                TableCellRenderer renderer,
                                int row, int col) {
           Component c = super.prepareRenderer(renderer,
                                      row, col);
           if (row % 2 == 0 && !isCellSelected(row,col)) {
             c.setBackground(Color.LIGHT_GRAY);
           } else {
             c.setBackground(getBackground());
           }
           return c;
         }
   
         public static void main(String[] args) {
           SwingUtilities.invokeLater(new Runnable() {
             public void run() {
               new PrintableColorTable();
             }
           });
         }
         // common code here . . .
  } 

PrintableColorTable displays and prints a JTable with the background of odd rows set to light grey. There are times, however, when you want the format of the printed table to be different than that of the displayed table. For example, you might want to retain the striped look in the displayed version of the table, but not retain the striped look in the printout. Swing engineer, Shannon Hickey suggested the following approach to displaying and printing a JTable using different formats see the JavaDesktop forums thread JTable.print() - different TableCellRenderer.

In Shannon's approach you introduce a boolean that tracks whether you are currently printing. You then override the JTable.print(Graphics g) method to toggle the boolean, as follows:

   public void print(Graphics g) {
     printing = true;
     try {
       super.print(g);
     }  finally {
       printing = false;
     }
   }

Next, test for the boolean when you set the background color of the cell:

   if (row % 2 == 0 && !isCellSelected(row,col)) {
       c.setBackground(Color.LIGHT_GRAY);
   // cell is selected, use the highlight color
   } else if (isCellSelected(row, col)) {
       c.setBackground(getSelectionBackground());
   } else {
       c.setBackground(getBackground());
   } 

The following class, FancyPrintableColor, put these steps together. It displays the shaded version of the JTable, but prints the non-shaded version:

   import javax.swing.JTable;
   import javax.swing.SwingUtilities;
   import javax.swing.JFrame;
   import javax.swing.JPanel;
   import javax.swing.JScrollPane;
   import javax.swing.table.TableCellRenderer;
   import java.awt.Dimension;
   import java.awt.GridLayout;
   import java.awt.Component;
   import java.awt.Color;
   import java.awt.Graphics;
   import java.awt.print.PrinterException;
   
   public class FancyPrintableColorTable extends JTable {
       private boolean printing = false;
   
         public FancyPrintableColorTable() {
           super(data, columnNames);
           setUpJFrame();
           try {
             print();
           } catch (PrinterException e) {
             e.printStackTrace();
           }
         }
      public void print(Graphics g) {
         printing = true;
         try {
           super.print(g);
         }  finally {
           printing = false;
         }
       }
   
      public Component prepareRenderer(
                                TableCellRenderer renderer,
                                int row, int col) {
           Component c = super.prepareRenderer(renderer,
                                      row, col);
           // if printing, only use plain background
           if (printing) {
              c.setBackground(getBackground());
           } else {
               if (row % 2 == 0 && !isCellSelected(row,col)) {
                   c.setBackground(Color.LIGHT_GRAY);
           // cell is selected, use the highlight color
           } else if (isCellSelected(row, col)) {
               c.setBackground(getSelectionBackground());
           } else {
               c.setBackground(getBackground());
           }
         }    
           return c;
        }
   
         public static void main(String[] args) {
           SwingUtilities.invokeLater(new Runnable() {
             public void run() {
               new FancyPrintableColorTable();
             }
           });
         }
       // common code here . . .
   } 

For more information on the new support in J2SE 5.0 for printing tables, see JTable needs printing support in Swing API changes in JDK 5.0.

FROM STRINGTOKENIZER TO SCANNER

The December 1, 2004 tip Scanning Text With java.util.Scanner, described how to use the new J2SE 5.0 Scanner class to read and parse text-based files. One question that has been asked is how does the Scanner class differ from the java.util.StringTokenizer class? The following tip addresses that question. It shows how to use the StringTokenizer class to perform some of the actions covered in the Scanner tip. The following tip also demonstrates that rather than using StringTokenizer, it's better to use the split() function included in the String class, or move to other regular expression-based solutions such as Scanner.

The final example in the Scanner tip, showed how to use the Scanner class to parse a data file that contains an employee record on each line. Each employee record contains the employee's first name, age, and a boolean that indicates whether the employee is certified. The values are separated by commas. A typical line might look like this:

   Joe,38,true

You can parse this line using the Scanner class with a program that looks something like the following:

   import java.util.Scanner;
   import java.io.File;
   import java.io.FileNotFoundException;

   public class DataScanner {

     private static void parseLine(String line) {
       Scanner lineScanner = new Scanner(line);
       lineScanner.useDelimiter("\\s*,\\s*");
       String name = lineScanner.next();
       int age = lineScanner.nextInt();
       boolean isCertified = lineScanner.nextBoolean();

       if (isCertified)
         System.out.println(name + " is certified.");
       if (age > 30)
         System.out.println(name + " is old enough.");
     }

     public static void main(String[] args) {
       if (args.length != 1) {
         System.err.println("usage: java DataScanner"
           + " \"Name,age,boolean\"");
         System.exit(0);
       }
       parseLine(args[0]);
     }
   }

Compile DataScanner. Then run it from the command line like this:

   java DataScanner "Joe,38,true"

You should get the following output:

   Joe is certified.
   Joe is old enough.

The same function can be performed with the StringTokenizer. To do that, you change parseLine to tokenize based on the "," and call for the nextToken(). Here's what the code would look like:

   import java.util.StringTokenizer;

   public class DataTokenizer {

     private static void parseLine(String line) {
       StringTokenizer tokenizer =
                          new StringTokenizer(line, ",");
       String name = tokenizer.nextToken();
       int age = Integer.parseInt(tokenizer.nextToken());
       boolean isCertified =
             Boolean.parseBoolean(tokenizer.nextToken());
             
       if (isCertified)
         System.out.println(name + " is certified.");
       if (age > 30)
         System.out.println(name + " is old enough.");
     }

     public static void main(String[] args) {
       if (args.length != 1) {
         System.err.println("usage: java DataTokenizer"
           + " \"Name,age,boolean\"");
         System.exit(0);
       }
       parseLine(args[0]);
     }
   }

Run DataTokenizer on the same data as in the previous example:

   java DataTokenizer "Joe,38,true"

and you will get the same result:

   Joe is certified.
   Joe is old enough.

However, if you introduce a space between the values, the tokenizer fails. Try running the following:

   java DataTokenizer "Joe, 38, true"

You will see a stack trace for the parsing exception.

You might argue that for comma-separated values (CSVs) there should not be extra spaces. That is true, however, as Ian Darwin points out in the Java Cookbook, it is not uncommon to encounter quotation marks or escaped quotation marks in this setting. Darwin provides a Java version of a Kernighan and Pike C++ program to demonstrate the amount of programming necessary to parse CSVs if you do not take advantage of regular expressions.

In fact, regular expression functionality has been added to the String class. The J2SE 5.0 JavaDocs now explicitly discourage future use of StringTokenizer, noting that:

  StringTokenizer is a legacy class that is retained for 
  compatibility reasons although its use is discouraged in new 
  code. It is recommended that anyone seeking this functionality 
  use the split method of String or the java.util.regex package 
  instead.

Using split() in String, the DataTokenizer example code would look like this.

   public class DataSplitter {

     private static void parseLine(String line) {
       String[] token = line.split("\\s*,\\s*");
       String name = token[0];
       int age = Integer.parseInt(token[1]);
       boolean isCertified = Boolean.parseBoolean(token[2]);

       if (isCertified)
         System.out.println(name + " is certified.");
       if (age > 30)
         System.out.println(name + " is old enough.");
     }

     public static void main(String[] args) {
       if (args.length != 1) {
         System.err.println("usage: java DataSplitter"
           + " \"Name,age,boolean\"");
         System.exit(0);
       }
       parseLine(args[0]);
     }
   }

A straightforward port would instead use the line:

   String[] token = line.split(",");

Here you can take advantage of regular expressions and accommodate any number of spaces on either side of the ",". You do this by adding "\\s*" before and after the "," in the regular expression. In any case, "token" is an array of strings. The remaining code in DataSplitter converts the second String to an int, converts the third String to a boolean, and outputs the results.

Here are the three lines that were in the Employee.data file that was used in the Scanner tip:

   Joe,38,true
   Kay,27,true
   Lou,33,false

A program in the Scanner tip used nested scanners to parse the data in the Employee.data file. Here is how you might do the parse through nested uses of split():

   import java.util.Scanner;
   import java.io.File;
   import java.io.FileNotFoundException;

   public class DataStringSplitter {
     private static String name;
     private static int age;
     private static boolean isCertified;

     private static String getFile(String fileName) {
       Scanner scanner = null;
       try {
         scanner = new Scanner(new File(fileName));
         scanner.useDelimiter("\\z");
         return scanner.next();

       } catch (FileNotFoundException e) {
         e.printStackTrace();
         return "";
       } finally {
         scanner.close();
       }
     }

     private static void readFile(String file) {
       String[] line =
         file.split(System.getProperty("line.separator"));
       for (String s : line) { parseLine(s); }
     }

     private static void parseLine(String line) {
       String[] data = line.split("\\s*,\\s*");
       name = data[0];
       age = Integer.parseInt(data[1]);
       isCertified = Boolean.parseBoolean(data[2]);
       if (isCertified)
         System.out.println(name + " is certified.");
       if (age > 30)
         System.out.println(name + " is old enough.");
     }

     public static void main(String[] args) {
       if (args.length != 1) {
         System.err.println("usage: java DataStringSplitter"
           + " file_location");
         System.exit(0);
       }
       readFile(getFile(args[0]));
     }
   }

The getFile() method in DataStringSplitter uses a Scanner technique introduced in the Scanner tip to store the entire file as a single String. The parseLine() method is the same as before, and is called by the readFile() method on each line:

   private static void readFile(String file) {
      String[] line =
        file.split(System.getProperty("line.separator"));
      for (String s : line) { parseLine(s); }
    }

The file is tokenized into Strings by splitting on the line separator string for your platform. Each line is saved as a separate String object in the array named "line". Then, using the enhanced for loop, the program iterates through the strings, and parses them using parseLine().

Compile DataStringSplitter. Then run it with the following command:

   java DataStringSplitter Employee.data

You should get the following output:

   Joe is certified.
   Joe is old enough.
   Kay is certified.
   Lou is old enough.

The Scanner approach is slightly different. In this approach, the readFile() method can take advantage of the nextLine() and hasNextLine() methods in the Scanner class:

    private static void readFile(String file) {
       Scanner scanner = new Scanner(file);
       while (scanner.hasNextLine()) {
         parseLine(scanner.nextLine());
       }
     }

In parseLine, you can tokenize the String using the same regular expression that was used with the split() method in the previous example. The advantage is that you can ask whether the next token can be converted from a String to a value of that type. For example, you can ask if the next token can be treated as an int using hasNextInt(). If the answer is yes, you can convert it to an int using the nextInt() method.

Here is the more robust DataString Scanner example:

   import java.util.Scanner;
   import java.io.File;
   import java.io.FileNotFoundException;

   public class DataStringScanner {

     private static String name;
     private static int age;
     private static boolean isCertified;

     private static String getFile(String fileName) {
       Scanner scanner = null;
       try {
         scanner = new Scanner(new File(fileName));
         scanner.useDelimiter("\\z");
         return scanner.next();

       } catch (FileNotFoundException e) {
         e.printStackTrace();
         return "";
       } finally {
         scanner.close();
       }
     }

     private static void readFile(String file) {
       Scanner scanner = new Scanner(file);
       while (scanner.hasNextLine()) {
         parseLine(scanner.nextLine());
       }
     }

     private static void parseLine(String line) {
       Scanner lineScanner = new Scanner(line);
       lineScanner.useDelimiter("\\s*,\\s*");
       name = lineScanner.next();
       if (lineScanner.hasNextInt())
         age = lineScanner.nextInt();
       else
         lineScanner.next();
       if (lineScanner.hasNextBoolean())
         isCertified = lineScanner.nextBoolean();
       else
         lineScanner.next();
       if (isCertified)
         System.out.println(name + " is certified.");
       if (age > 30)
         System.out.println(name + " is old enough.");
     }

     public static void main(String[] args) {
       if (args.length != 1) {
         System.err.println("usage: java DataStringScanner"
           + " file_location");
         System.exit(0);
       }
       readFile(getFile(args[0]));
     }
   }

Compile DataStringScanner. Then run it with the following command:

   java DataStringScanner Employee.data

Again, you should see the following output:

   Joe is certified.
   Joe is old enough.
   Kay is certified.
   Lou is old enough.   

This tip demonstrated that you can parse many strings using the StringTokenizer. However, it also suggested that you use regular expression-based solutions for future development. This includes the split() method in the String class as well as various nextXXX(), and hasNextXXX() in the Scanner class. The Scanner class gives you a great deal of power, including the ability to enforce the type represented by individual tokens.

For more information on Scanner, see the Tech Tip Scanning Text With java.util.Scanner. Also see the formal documentation for the Scanner class.

.
.
Rate and Review
Tell us what you think of the content of this page.
Excellent   Good   Fair   Poor  
Comments:
If you would like a reply to your comment, please submit your email address:
Note: We may not respond to all submitted comments.
.
.

IMPORTANT: Please read our Licensing, Terms of Use, and Privacy policies:
http://developer.java.sun.com/berkeley_license.html
http://www.sun.com/share/text/termsofuse.html

Privacy Statement: Sun respects your online time and privacy (http://sun.com/privacy). You have received this based on your email preferences. If you would prefer not to receive this information, please follow the steps at the bottom of this message to unsubscribe.

Comments? Send your feedback on the Core Java Technologies Tech Tips to: http://developers.sun.com/contact/feedback.jsp?category=newslet

Subscribe to other Java developer Tech Tips:

- Enterprise Java Technologies Tech Tips. Get tips on using enterprise Java technologies and APIs, such as those in the Java 2 Platform, Enterprise Edition (J2EE).
- Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2ME).

To subscribe to these and other Sun Developer Network publications:
- Go to the Sun Developer Network Subscriptions page, choose the newsletters you want to subscribe to and click "Submit".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Submit".


ARCHIVES: You'll find the Core Java Technologies Tech Tips archives at:
http://java.sun.com/developer/JDCTechTips/index.html


Copyright 2005 Sun Microsystems, Inc. All rights reserved.
4150 Network Circle, Santa Clara, CA 95054 USA.


This document is protected by copyright. For more information, see:
http://java.sun.com/developer/copyright.html


Java, J2SE, J2EE, J2ME, and all Java-based marks are trademarks or registered trademarks (http://www.sun.com/suntrademarks/) of Sun Microsystems, Inc. in the United States and other countries.

Sun Microsystems,
Inc.
.
.