User interfaces for object-oriented systems, Part 5: Useful stuff

Build an application that puts user-interface principles into practice

1 2 3 4 5 Page 4
Page 4 of 5
List 7. /src/com/holub/io/Log.java
   1: package com.holub.io;
   2: 
   3: import java.io.*;
   4: import java.util.*;
   5: import com.holub.io.Std;
   6: import com.holub.tools.debug.Assert;
   7: // Import com.holub.tools.Assert;
   8: 
   9: /** Encapsulates I/O to a log file. Contains simple wrappers to do
  10:  *  line-level buffering and suck up exceptions that are not errors
  11:  *  in the current context. The Log class encapsulates a Writer
  12:  *  rather than extending it in order to provide more reasonable
  13:  *  exception handling. A Log.Failure object (which is a
  14:  *  RuntimeException) is thrown when something goes wrong
  15:  *  rather than an IOException. This way you don't have to
  16:  *  litter up your code with try blocks. The {@link #writer} method is
  17:  *  provided to allow access to the underlying Writer should
  18:  *  you need it, however.
  19:  *  <p>
  20:  *  This class is thread safe.
  21:  **/
  22: 
  23: public class Log
  24: {
  25:   private Writer                  log_file        = null;
  26:   private static /*final*/ String log_file_name   = "log";
  27:   private boolean                 timestamp       = false; 
  28: 
  29:   public class Failure extends RuntimeException
  30:   {   public Failure(String s){super(s);}
  31:     }
  32: 
  33:     /*******************************************************************
  34:      * Create a new Log with the specified name. If a file with that
  35:      * name exists, then new lines are just appended to it. The current
  36:      * time and date are written into the file as well.
  37:      * <p>
  38:      * An error message is printed if any exceptions are thrown from
  39:      * the I/O system, but the program is permitted to run. (In this
  40:      * case, any subsequent attempts to write to the log are silently
  41:      * ignored.)
  42:      *
  43:      * @param name file to which log messages are sent. If null,
  44:      *              log messages go to standard error.
  45:      * @param error_message_stream. Any error messages that indicate
  46:      *              problems opening the file are sent here.
  47:      *              Use Std.bit_bucket() to suppress error messages.
  48:      */
  49: 
  50:   public Log( String name, PrintWriter error_message_stream )
  51:     {   Assert.is_true( name != null );
  52:         Assert.is_true( error_message_stream != null );
  53: 
  54:         log_file_name = name;
  55:         try
  56:         {   
  57:             error_message_stream.println("Logging to " + name);
  58: 
  59:             log_file = ( name == null )
  60:                     ? (Writer)( Std.err() )
  61:                     : (Writer)( new BufferedWriter(
  62:                             new FileWriter(log_file_name, true /*append*/)) )
  63:                     ;
  64: 
  65:             write( "\n#----" + new Date().toString() + "----#\n" );
  66:         }
  67:         catch( IOException e )
  68:         {
  69:             error_message_stream.println("Couldn't open log file: " 
  70:                                + log_file_name
  71:                                + "(" + e.getMessage() + ")" );
  72: 
  73:             error_message_stream.println("Input will not be logged\n");
  74:         }
  75:     }
  76: 
  77:     /*******************************************************************
  78:      * Writes to a previously opened log file.
  79:      */
  80: 
  81:   public Log( Writer log_file )
  82:     {   Assert.is_true( log_file != null );
  83:         this.log_file      = log_file;
  84:         this.log_file_name = "???";
  85:         write( "\n#----" + new Date().toString() + "----#\n" );
  86:     }
  87: 
  88:     /*******************************************************************
  89:      * Convenience method. Creates a log called "log" in the directory
  90:      * specified by the log.file system property. If the log.file
  91:      * System property (which must be specified with the -D command-line
  92:      * switch) can't be found or if the file can't be opened, then log output
  93:      * is silently discarded.
  94:      */
  95: 
  96:   public Log()
  97:     {   this( System.getProperty("log.file"), Std.err() );
  98:     }
  99: 
 100:     /*******************************************************************
 101:      * All messages logged after this call is made will be prefixed by
 102:      * the current date and time. (This stamp is in addition to the one
 103:      * that's output at the top of the file, which effectively indicates
 104:      * when the log file was opened.)
 105:      */
 106: 
 107:   public void timestamp()
 108:     {   timestamp = true;
 109:     }
 110: 
 111:     /*******************************************************************
 112:      * Append a string of the indicated length, starting at the indicated
 113:      * offset, to the log file. All internal buffers are flushed
 114:      * after writing so that the log file itself will be up-to-date.
 115:      * An error message is printed if any exceptions are thrown from
 116:      * the I/O system.
 117:      * <p>
 118:      * A Failure is thrown if any exceptions are thrown from any internal
 119:      * Writer calls.
 120:      */
 121: 
 122:   public void write( String text, int offset, int length )
 123:     {   Assert.is_true( text != null                            );
 124:         Assert.is_true( offset >= 0                             );
 125:         Assert.is_true( 0 <= length && length <= text.length()  );
 126: 
 127:         if( log_file == null ) return;  // do nothing if create failed  
 128: 
 129:         try
 130:         {   if( timestamp )
 131:                 log_file.write( new Date().toString() + ": " );
 132: 
 133:             log_file.write( text, offset, length );
 134:             log_file.flush();
 135:         }
 136:         catch( IOException e )
 137:         {   throw new Failure( log_file_name + ": " +
 138:                                                 e.getMessage() );
 139:         }
 140:     }
 141:     /*******************************************************************
 142:      * Convenience method. Logs the entire string.
 143:      */
 144:   public synchronized void write( String text )
 145:     {   this.write( text, 0, text.length() );
 146:     }
 147: 
 148:     /*******************************************************************
 149:      * Close the log file.
 150:      * A Failure is thrown if any exceptions are thrown from
 151:      * the I/O system.
 152:      */
 153:   public synchronized void close( )
 154:     {   if( log_file == null ) return;  // do nothing if create failed  
 155: 
 156:         try
 157:         {   log_file.close();
 158:         }
 159:         catch( IOException e )
 160:         {   throw new Failure( log_file_name + ": " +
 161:                                                 e.getMessage() );
 162:         }
 163:     }
 164: 
 165:     /********************************************************************
 166:      * Objects of the Log.Writer_accessor are returned from the {@link #writer}
 167:      * method. They implement the Writer interface and chain most operations
 168:      * through to the underlying writer. They provide synchronized access to the
 169:      * Writer as well. (That is, multiple threads can safely and simultaneously
 170:      * access both the Log object itself and also any
 171:      * Writers returned from various writer()
 172:      * calls on that Log object). The only surprise is the
 173:      * {@link #close} method, which throws a
 174:      * java.lang.UnsupportedOperationException. You must close the
 175:      * encapsulating Log object, not the object returned from {@link #writer}.
 176:      *
 177:      * Note that:
 178:      *  
 179:      *  (1) The log file is flushed after every write operation (including
 180:      *      single-character writes) to make sure that it's as up-to-date as possible.
 181:      *  (2) If the timestamp() method of the encapsulating Log
 182:      *      has been called, output sent to the Log using either String
 183:      *      version of write() is time stamped, but output sent using
 184:      *      the character versions of write() are not time stamped.
 185:      *  
 186:      */
 187: 
 188:   private class Writer_accessor extends Writer
 189:     {
 190:       public void write( int c ) throws IOException
 191:         {   synchronized(Log.this)
 192:             {   log_file.write(c);
 193:                 log_file.flush();
 194:             }
 195:         }
 196:       public void write( char[] cbuf ) throws IOException
 197:         {   synchronized(Log.this)
 198:             {   log_file.write(cbuf);
 199:                 log_file.flush();
 200:             }
 201:         }
 202:       public void write( char[] cbuf, int off, int len ) throws IOException
 203:         {   synchronized(Log.this)
 204:             {   log_file.write(cbuf,off,len);
 205:                 log_file.flush();
 206:             }
 207:         }
 208:       public void write( String str ) throws IOException
 209:         {   Log.this.write(str);
 210:         }
 211:       public void write( String str, int off, int len ) throws IOException
 212:         {   Log.this.write(str,off,len);
 213:         }
 214:       public void flush() throws IOException
 215:         {   synchronized(Log.this)
 216:             {   log_file.flush();
 217:             }
 218:         }
 219:       public void close()
 220:         {   throw new java.lang.UnsupportedOperationException
 221:                                     ("Must close encasulating Log object");
 222:         }
 223:     }
 224: 
 225:     /** Return a Writer that writes to the current log. This writer is a
 226:      *  singleton in that, for a given Log object, only one
 227:      *  Writer is created. All calls to writer() return
 228:      *  references to the same object.
 229:      */
 230: 
 231:   public Writer writer()
 232:     {   if( the_writer == null )
 233:         {   synchronized( this )
 234:             {   if( the_writer == null )
 235:                     the_writer = new Writer_accessor();
 236:             }
 237:         }
 238:         return the_writer;
 239:     }
 240: 
 241:   private static Writer_accessor the_writer;
 242: 
 243: 
 244:     /**********************************************************************
 245:      * A Unit test class.
 246:      */
 247: 
 248:   public static class Test
 249:   {   public static void main( String[] args )
 250:         {   
 251:             Log log = new Log( "log.test", Std.err() );
 252: 
 253:             log.write( "hello world\n" );
 254:             log.write( "xxxhello world\n", 3, 12 );
 255: 
 256:             log.timestamp();
 257: 
 258:             log.write( "timestamp now on\n" );
 259:             log.write( "xxxhello world\n", 3, 12 );
 260: 
 261:             try
 262:             {   
 263:                 Writer writer = log.writer();
 264:                 writer.write( "output directly to writer\n" );
 265:                 writer.write( new char[]{ 'c', 'h', 'a', 'r', ' ', 'o', 'u', 't', '\n'} );
 266:             }
 267:             catch( IOException exception )
 268:             {   exception.printStackTrace();
 269:             }
 270:         }
 271:     }
 272: }
         

String alignment

String manipulation is one of Java's major weaknesses, both because it's so inefficient and because much of the string-manipulation methods needed by real programs are simply missing. If you're doing hard-core string work, Daniel F. Savarese has generously made his Perl-style regular-expression package available (see Resources), but often a full-blown regular-expression system is overkill. I've created a package of small string-manipulation utilities that might be the subject of a future column, but the one I need for the current calculator application is the Align utility, whose methods align strings within columns.

For example, Align.right( "123", 10 ) returns a 10-character string with "123" right-aligned within it. (The first seven characters of the string are space characters.) The Align utility also supports the Align.left( "123", 10 ) and Align.center( "123", 10 ) methods, which do the expected.

One additional alignment method -- Align.align( "123.45", 10, 5, '.', ' ' ); -- outputs a 10-character string and places the period in "123.45" in column 5. Align uses spaces (specified in the last argument) as fill characters.

The following code:

Std.out().println( "[0123456789]" );
Std.out().println( "[" + Align.align("123.45", 10, 5, '.', ' ') + "]");
Std.out().println( "[" + Align.align("1.2",    10, 5, '.', ' ') + "]");
Std.out().println( "[" + Align.align("12.345", 10, 5, '.', ' ') + "]");

prints:

[0123456789]
[  123.45  ]
[    1.2   ]
[   12.345 ]

The periods are aligned vertically.

The code is in List 8.

List 8. /src/com/holub/string/Align.java
   1: package com.holub.string;
   2: 
   3: public class Align
   4: {
   5:     /*****************************************************************
   6:      * Aligns the text so that the first instance of the align_on character
   7:      * is positioned at align_column, with the pad_character added
   8:      * to both the left and right of the string to make it work.
   9:      * If the align_on character isn't found in the input string, then
  10:      * the output is right adjusted at column alignment_column - 1.
  11:      * For example:
  12:      * 
  13:      *                                             align_column
  14:      *                                                    |
  15:      *                                                    V
  16:      *                                                  01234
  17:      *  align( "1.2"  , 5, 2, '.', '_');    // returns  _1.2_   
  18:      *  align( ".23"  , 5, 2, '.', '_');    // returns  __.23   
  19:      *  align( "12.3" , 5, 2, '.', '_');    // returns  12.3_
  20:      *  align( "2.34" , 5, 2, '.', '_');    // returns  _2.34   
  21:      *  align( "12.34", 5, 2, '.', '_');    // returns  12.34   
  22:      *  align( "1",     5, 2, '.', '_');    // returns  _1___   
  23:      * 
  24:      *
  25:      * @param input         The String to align.
  26:      * @param column_width  The width of the output string.
  27:      * @param align_column  The column at which the align_on character
  28:      *                      is to be positioned.
  29:      * @param align_on      The leftmost instance of this character in
  30:      *                      the input string is aligned at this column.
  31:      *                      If Align.LAST, then the rightmost character in the
  32:      *                      string is used. If Align.FIRST, then the leftmost
  33:      *                      character in the string is used.
  34:      * @param pad_character The character to use for padding.
  35:      * @return The input string, with left and right padding added as needed.
  36:      */
  37: 
  38:    static public int FIRST = 0;
  39:    static public int LAST  = -1;
  40: 
  41:    static public String align( String input, int  column_width
  42:                                              , int  align_column
  43:                                              , int  align_on
  44:                                              , char pad_character )
  45:      {
  46:         int align_character_at = (align_on == FIRST) ? 0                      :
  47:                                  (align_on == LAST ) ? input.length()-1       :
  48:                                                        input.indexOf(align_on);
  49:         int left_padding  = align_character_at >= 0
  50:                             ? Math.max( 0, align_column - align_character_at )
  51:                             : Math.max( 0, align_column - input.length() )
  52:                             ;
  53: 
  54:         int right_padding = column_width - left_padding - input.length();
  55: 
  56:         return do_alignment( left_padding, input, right_padding, pad_character );
  57:     }
  58: 
  59:     /**********************************************************************
  60:      * Build the output string with the required padding.
  61:      */
  62:   static private String do_alignment(int left_padding, String input,
  63:                                     int right_padding, char pad_character)
  64:     {
  65:         StringBuffer work = new StringBuffer();
  66: 
  67:         while( --left_padding >= 0 )
  68:             work.append( pad_character );
  69: 
  70:         work.append( input );
  71: 
  72:         while( --right_padding >= 0 )
  73:             work.append( pad_character );
  74: 
  75:         return work.toString();
  76:      }
  77: 
  78:     /**********************************************************************
  79:      * Convenience method -- pads with spaces, left-adjusting the text.
  80:      */
  81:   static public String left( String input, int column_width )
  82:     {   return align( input, column_width, 0, FIRST, ' ' );
  83:     }
  84: 
  85:     /**
  86:      * Convenience method -- pads with spaces, right-adjusting the text.
  87:      */
  88:   static public String right( String input, int column_width )
  89:     {   return align( input, column_width, column_width-1, LAST, ' ' );
  90:     }
  91: 
  92:     /**
  93:      * Pads the input string with the pad_character so that it
  94:      * is column_width characters wide and centered in the column.
  95:      * If it can't be centered exactly, the extra padding is placed
  96:      * on the left.  For example, if the input string is "abc" and
  97:      * you center it in a 6-character-wide column with '.' as the
  98:      * pad_character, then the output string is "..abc.".
  99:      */
 100:   static public String center( String input, int column_width, char pad_character )
 101:     {   int need = column_width - input.length();
 102:         return ( need > 0 )
 103:                 ? do_alignment( need - (need/2), input, need/2, pad_character )
 104:                 : input
 105:                 ;
 106:     }
 107: 
 108:     /** Convenience method, pads with spaces.
 109:     */
 110: 
 111:   static public String center( String input, int column_width )
 112:     {   return center( input, column_width, ' ' );
 113:     }
 114:       
 115: 
 116:   private static class Test
 117:     {   
 118:       public static void main(String[] args)
 119:         {
 120:         com.holub.tools.Tester t = 
 121:                 new com.holub.tools.Tester( args.length > 0,
 122:                                             com.holub.io.Std.out() );
 123: 
 124:         t.check( "align.1", "+1.2+", align( "1.2"  , 5, 2, '.', '+') );
 125:         t.check( "align.2", "++.23", align( ".23"  , 5, 2, '.', '+') );
 126:         t.check( "align.3", "12.3+", align( "12.3" , 5, 2, '.', '+') );
 127:         t.check( "align.4", "+2.34", align( "2.34" , 5, 2, '.', '+') );
 128:         t.check( "align.5", "12.34", align( "12.34", 5, 2, '.', '+') );
 129:         t.check( "align.6", "+1+++", align( "1",     5, 2, '.', '+') ); 
 130:         t.check( "align.7", "+1.++", align( "1.",    5, 2, '.', '+') ); 
 131: 
 132:         t.check( "align.8", "01234", left ( "01234",    5       )   );
 133:         t.check( "align.9", "     ", left ( "",         5       )   );
 134:         t.check( "align.a", "X    ", left ( "X",        5       )   );
 135: 
 136:         t.check( "align.b", "01234", right( "01234",    5       )   );
 137:         t.check( "align.c", "    X", right( "X",        5       )   );
 138: 
 139:         t.check( "align.d", "  X  ", center("X",        5       )   );
 140:         t.check( "align.e", "  XX ", center("XX",       5       )   );
 141:         t.check( "align.e", "XXXXX", center("XXXXX",    5       )   );
 142:         t.exit();
 143:         }
 144:     }
 145: }
         

Adding scroll bars to a text area.

The final workhorse class of interest to the calculator app is the Scrollable_JTextArea (List 9, line 13).

This is a simple convenience class that compensates for what I consider to be a flaw of Swing's JTextArea: it doesn't create scroll bars when it's too small to fully display its contents. I've provided a small class that extends JScrollPane to automatically create an internal JTextArea. Although the Scrollable_JTextArea is a JScrollPane, it implements many of the methods of JTextArea, which are simple pass-throughs to the contained text widget. That is, you can just pass the Scrollable_JTextArea object messages such as setText(), append(), and getText(), as if it were a JTextArea.

The main problem with making JScrollPane the base class is that, from a design point of view, you really want this thing to be a JTextArea with scroll bars, not a scroll pane. Implementing JScrollPane is awkward. The alternative would be to extend JTextArea and literally override everything, but that solution is both tedious and ugly.

The code is in List 9.

1 2 3 4 5 Page 4
Page 4 of 5