Tuesday, January 19, 2010

Section 20.13. File Viewer Finis










20.13. File Viewer Finis




As a final example of working with readers and writers,
we return for the last time to the FileDumper application last seen in Chapter 18. At that point, we had a GUI program that allowed any file to be opened and interpreted in one of several formats, including ASCII, decimal, hexadecimal, short, regular, and long integers in both big- and little-endian formats, floating point, and double-precision floating point.


In this section, we expand the program to read many different text formats besides ASCII. The user interface must be adjusted to allow a binary choice of whether the file contains text or numeric data. If a user chooses text, a reader reads the file instead of an input stream. We also need a way for the user to pick the text encoding (e.g., MacRoman, Latin-1, Unicode, etc). Since there are several dozen text encodings, the best choice is a list box. All of this can be integrated into the mode panel. Figure 20-1 shows the revised TextModePanel class. The code is given in Example 20-9. I've added two new public methods, isText( ) and getEncoding( ). The rest of the changes are fairly minor ones to set up the GUI.



Figure 20-1. A mode panel with a list box for encodings



Example 20-9. TextModePanel




import java.awt.*;
import javax.swing.*;
import java.nio.charset.*;
import java.util.*;
public class TextModePanel extends JPanel {
private JCheckBox bigEndian = new JCheckBox("Big Endian", true);
private JCheckBox deflated = new JCheckBox("Deflated", false);
private JCheckBox gzipped = new JCheckBox("GZipped", false);
private ButtonGroup dataTypes = new ButtonGroup( );
private JRadioButton asciiRadio = new JRadioButton("Text");
private JRadioButton decimalRadio = new JRadioButton("Decimal");
private JRadioButton hexRadio = new JRadioButton("Hexadecimal");
prviate JRadioButton shortRadio = new JRadioButton("Short");
private JRadioButton intRadio = new JRadioButton("Int");
private JRadioButton longRadio = new JRadioButton("Long");
private JRadioButton floatRadio = new JRadioButton("Float");
private JRadioButton doubleRadio = new JRadioButton("Double");
private JTextField password = new JPasswordField( );
private JList encodings = new JList( );
public TextModePanel( ) {
Map charsets = Charset.availableCharsets( );
encodings.setListData(charsets.keySet().toArray( ));
this.setLayout(new GridLayout(1, 2));
JPanel left = new JPanel( );
JScrollPane right = new JScrollPane(encodings);
left.setLayout(new GridLayout(13, 1));
left.add(bigEndian);
left.add(deflated);
left.add(gzipped);
left.add(asciiRadio);
asciiRadio.setSelected(true);
left.add(decimalRadio);
left.add(hexRadio);
left.add(shortRadio);
left.add(intRadio);
left.add(longRadio);
left.add(floatRadio);
left.add(doubleRadio);
dataTypes.add(asciiRadio);
dataTypes.add(decimalRadio);
dataTypes.add(hexRadio);
dataTypes.add(shortRadio);
dataTypes.add(intRadio);
dataTypes.add(longRadio);
dataTypes.add(floatRadio);
dataTypes.add(doubleRadio);
left.add(password);
this.add(left);
this.add(right);
}
public boolean isBigEndian( ) {
return bigEndian.isSelected( );
}
public boolean isDeflated( ) {
return deflated.isSelected( );
}
public boolean isGZipped( ) {
return gzipped.isSelected( );
}
public boolean isText( ) {
if (this.getMode( ) == FileDumper6.ASC) return true;
return false;
}
public String getEncoding( ) {
return (String) encodings.getSelectedValue( );
}
public int getMode( ) {
if (asciiRadio.isSelected( )) return FileDumper6.ASC;
else if (decimalRadio.isSelected( )) return FileDumper6.DEC;
else if (hexRadio.isSelected( )) return FileDumper6.HEX;
else if (shortRadio.isSelected( )) return FileDumper6.SHORT;
else if (intRadio.isSelected( )) return FileDumper6.INT;
else if (longRadio.isSelected( )) return FileDumper6.LONG;
else if (floatRadio.isSelected( )) return FileDumper6.FLOAT;
else if (doubleRadio.isSelected( )) return FileDumper6.DOUBLE;
else return FileDumper6.ASC;
}
public String getPassword( ) {
return password.getText( );
}
}



Next we should fix an unrecognized bug in the earlier program. It used an OutputStream to stream data into the text area. It converted the bytes to chars simply by casting them as if they were Latin-1. This works for the simple ASCII output needed to represent numbers, but the whole point of this chapter has been that this hack just doesn't work for more realistic text that can include content from many different languages. Thus we need to revise the JStreamedTextArea to stream to a Writer rather than an OutputStream. If anything, this is more straightforward. Example 20-10 demonstrates.


Example 20-10. The JWritableTextArea




package com.elharo.io.ui;
import javax.swing.*;
import java.awt.Font;
import java.io.*;
public class JWritableTextArea extends JTextArea {

private Writer writer = new BufferedWriter(new TextAreaWriter( ));
public JWritableTextArea( ) {
this("", 0, 0);
}
public JWritableTextArea(String text) {
this(text, 0, 0);
}
public JWritableTextArea(int rows, int columns) {
this("", rows, columns);
}
public JWritableTextArea(String text, int rows, int columns) {
super(text, rows, columns);
setFont(new Font("Monospaced", Font.PLAIN, 12));
setEditable(false);
}
public Writer getWriter( ) {
return writer;
}
public void reset( ) {
this.setText("");
writer = new BufferedWriter(new TextAreaWriter( ));
}
private class TextAreaWriter extends Writer {
private boolean closed = false;
public void close( ) {
closed = true;
}
public void write(char[] text, int offset, int length) throws IOException {
if (closed) throw new IOException("Write to closed stream");
JWritableTextArea.this.append(new String(text, offset, length));
}
public void flush( ) {}
}
}



Next we need to expand the FileDumper class to read and write text in a variety of encodings. This is straightforward and only requires one new overloaded dump( ) method, as shown in Example 20-11.


Example 20-11. FileDumper6




import java.io.*;
import java.util.zip.*;
import java.security.*;
import javax.crypto.*;
import javax.crypto.spec.*;
import com.elharo.io.*;
public class FileDumper6 {
public static final int ASC = 0;
public static final int DEC = 1;
public static final int HEX = 2;
public static final int SHORT = 3;
public static final int INT = 4;
public static final int LONG = 5;
public static final int FLOAT = 6;
public static final int DOUBLE = 7;
public static void dump(InputStream in, Writer out, int mode,
boolean bigEndian, boolean deflated, boolean gzipped, String password)
throws IOException {
// The reference variable in may point to several different objects
// within the space of the next few lines.
if (password != null && !password.equals("")) {
// Create a key.
try {
byte[] desKeyData = password.getBytes( );
DESKeySpec desKeySpec = new DESKeySpec(desKeyData);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey desKey = keyFactory.generateSecret(desKeySpec);
// Use Data Encryption Standard.
Cipher des = Cipher.getInstance("DES/ECB/PKCS5Padding");
des.init(Cipher.DECRYPT_MODE, desKey);
in = new CipherInputStream(in, des);
}
catch (GeneralSecurityException ex) {
throw new IOException(ex.getMessage( ));
}
}
if (deflated) {
in = new InflaterInputStream(in);
}
else if (gzipped) {
in = new GZIPInputStream(in);
}
if (bigEndian) {
DataInputStream din = new DataInputStream(in);
switch (mode) {
case HEX:
in = new HexFilter(in);
break;
case DEC:
in = new DecimalFilter(in);
break;
case INT:
in = new IntFilter(din);
break;
case SHORT:
in = new ShortFilter(din);
break;
case LONG:
in = new LongFilter(din);
break;
case DOUBLE:
in = new DoubleFilter(din);
break;
case FLOAT:
in = new FloatFilter(din);
break;
default:
}
}
else {
LittleEndianInputStream lin = new LittleEndianInputStream(in);
switch (mode) {
case HEX:
in = new HexFilter(in);
break;
case DEC:
in = new DecimalFilter(in);
break;
case INT:
in = new LEIntFilter(lin);
break;
case SHORT:
in = new LEShortFilter(lin);
break;
case LONG:
in = new LELongFilter(lin);
break;
case DOUBLE:
in = new LEDoubleFilter(lin);
break;
case FLOAT:
in = new LEFloatFilter(lin);
break;
default:
}
}
for (int c = in.read(); c != -1; c = in.read( )) {
out.write(c);
}
in.close( );
}
public static void dump(InputStream in, Writer out,
String inputEncoding, String outputEncoding, boolean deflated,
boolean gzipped, String password) throws IOException {
if (inputEncoding == null || inputEncoding.equals("")) {
inputEncoding = "US-ASCII";
}
if (outputEncoding == null || outputEncoding.equals("")) {
outputEncoding = System.getProperty("file.encoding", "8859_1");
}
// Note that the reference variable in
// may point to several different objects
// within the space of the next few lines.
if (password != null && !password.equals("")) {
try {
// Create a key.
byte[] desKeyData = password.getBytes( );
DESKeySpec desKeySpec = new DESKeySpec(desKeyData);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey desKey = keyFactory.generateSecret(desKeySpec);
// Use Data Encryption Standard.
Cipher des = Cipher.getInstance("DES/ECB/PKCS5Padding");
des.init(Cipher.DECRYPT_MODE, desKey);
in = new CipherInputStream(in, des);
}
catch (GeneralSecurityException ex) {
throw new IOException(ex.getMessage( ));
}
}
if (deflated) {
in = new InflaterInputStream(in);
}
else if (gzipped) {
in = new GZIPInputStream(in);
}
InputStreamReader isr = new InputStreamReader(in, inputEncoding);
int c;
while ((c = isr.read( )) != -1) {
out.write(c);
}
}
}



There's one new method in this class. An overloaded variant of dump( ) can be invoked to dump a text file in a particular encoding. This method accepts an input encoding string and an output encoding string as arguments. These are used to form readers and writers that interpret the bytes read from the file and written onto the output stream. Output encoding is optional. If it's omitted, the platform's default encoding is used.


The FileViewer2 class is straightforward. Aside from using a TextModePanel instead of a ModePanel, the only change it really requires is in the actionPerformed( ) method. Here you have to test whether the format is text or numeric and select the dump( ) method accordingly. Example 20-12 illustrates.


Example 20-12. FileViewer2




import javax.swing.*;
import java.io.*;
import com.elharo.io.ui.*;
import java.awt.*;
import java.awt.event.*;
public class FileViewer2 extends JFrame implements ActionListener {
JFileChooser chooser = new JFileChooser( );
JWritableTextArea theView = new JWritableTextArea( );
TextModePanel mp = new TextModePanel( );
public FileViewer2( ) {
super("FileViewer");
}
public void init( ) {
chooser.setApproveButtonText("View File");
chooser.setApproveButtonMnemonic('V');
chooser.addActionListener(this);
this.getContentPane( ).add(BorderLayout.EAST, chooser);
JScrollPane sp = new JScrollPane(theView);
sp.setPreferredSize(new Dimension(640, 400));
this.getContentPane( ).add(BorderLayout.SOUTH, sp);
this.getContentPane( ).add(BorderLayout.WEST, mp);
this.pack( );
// Center on display
Dimension display = getToolkit().getScreenSize( );
Dimension bounds = this.getSize( );
int x = (display.width - bounds.width)/2;
int y = (display.height - bounds.height)/2;
if (x < 0) x = 10;
if (y < 0) y = 15;
this.setLocation(x, y);
}
public void actionPerformed(ActionEvent evt) {
if (evt.getActionCommand( ).equals(JFileChooser.APPROVE_SELECTION)) {
File f = chooser.getSelectedFile( );
if (f != null) {
theView.reset( );
try {
InputStream in = new FileInputStream(f);
// This program was really slow until I buffered the stream.
in = new BufferedInputStream(in);
in = new ProgressMonitorInputStream(this, "Reading...", in);
if (!mp.isText( )) {
FileDumper6.dump(in, theView.getWriter(), mp.getMode( ),
mp.isBigEndian( ),
mp.isDeflated(), mp.isGZipped(), mp.getPassword( ));
}
else {
FileDumper6.dump(in, theView.getWriter(), mp.getEncoding( ), null,
mp.isDeflated(), mp.isGZipped(), mp.getPassword( ));
}
}
catch (IOException ex) {
JOptionPane.showMessageDialog(this, ex.getMessage( ),
"I/O Error", JOptionPane.ERROR_MESSAGE);
}
}
}
else if (evt.getActionCommand( ).equals(JFileChooser.CANCEL_SELECTION)) {
this.setVisible(false);
this.dispose( );
// This is a single window application
System.exit(0);
}
}
public static void main(String[] args) {
FileViewer2 viewer = new FileViewer2( );
viewer.init( );
viewer.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
viewer.setVisible(true);
}
}



Figure 20-2 shows the completed FileViewer application displaying a file full of Unicode text.



Figure 20-2. The final FileViewer application



This completes this program, at least as far as it will be taken in this book. You could certainly extend it further. For example, it would be a nice touch to add support for various image formats and perhaps even formatted text like HTML files. However, this would take us too far afield from the topic of this book, so I leave further improvements as exercises for the motivated reader.












No comments: