/**
 * VerCheck : Applet to check if applications have a new update available.
 *
 copyright (c) 2007-2008 Roedy Green, Canadian Mind Products
 may be copied and used freely for any purpose but military.
 Roedy Green

 Canadian Mind Products
 #101 - 2536 Wark Street
 Victoria, BC Canada
 V8T 4G8
 tel: (250) 361-9093
 roedy g at mindprod dotcom
 http://mindprod.com
 Version history
 1.0 2008-01-25 initial release
 1.1 2008-01-25 new Opera beta, add VerCheck itself
 1.2 2008-01-26 automatically restore default apps on every use, to automatically keep them up to date.
 1.3 2008-01-29 4NT incremented build number 8.02:102 to 8.02:103
 1.4 2008-01-30 4NT now repackaged as Take Command. Better thread isolation. Goldwave 5.23
 1.5 2008-02-01 new Take Command, new Opera Beta
 1.6 2008-02-01 remove obsolete entries. better check for Corel and Safari
 1.7 2008-02-03 new version Take Command, add iTunes
 1.8 2008-02-06 new version Take Command
 1.9 2008-02-07 Firefox 12.0.0.12 and Sea Monkey 1.1.8
 2.0 2008-02-09 Adobe Acrobat 1.1.2, requires regex and new Take Command
 2.1 2008-02-11 Bittorrent 6.0.2, new Take Command
 2.2 2008-02-14 new version FastStone 6.0
 2.3 2008-02-16 new version Take Command.  New icon for apps released in last week.
 2.4 2008-02-27 new Take Command, Boot-It, iTunes, Copernic, Opera
 2.5 2008-03-03 fix bug causing user apps to disappear, redo persistence, reorg way icons computed.
 2.6 2008-03-09 remove restore defaults button, many version change marker changes.
 2.7 2008-03-26 new firefox, sea monkey, intellij
 2.8 2008-04-17 many version detection string changes.
 2.9 2008-05-06 reorder columns so date last updated more visible.
 3.0 2008-08-20 new look under Vista with native fonts, and easier to read background.
 3.1 2008-08-31 all user to add a description field to each app. Export all data to HTML.
 3.2 2008-09-02 make sound work on Vista/JDK 1.6.0_10 and in application mode.
 3.3 2008-11-17 retry probes of apps that could not connect.
 */
package com.mindprod.vercheck;

import com.mindprod.common11.BigDate;
import com.mindprod.common11.Build;
import com.mindprod.common11.FontFactory;
import com.mindprod.common11.VersionCheck;
import com.mindprod.common13.CMPAboutJBox;
import com.mindprod.common13.Common13;
import com.mindprod.common13.HybridJ;
import com.mindprod.common13.JEButton;
import static com.mindprod.entities.InsertEntities.insertHTMLEntities;
import com.mindprod.http.Get;

import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import static java.lang.System.err;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
 * Check list of applications to see if any have changed versions. Created by IntelliJ IDEA.
 *
 * @author Roedy Green, (c) Canadian Mind Products
 * @version  3.3 2008-11-17 retry probes of apps that could not connect.
 */

// TODO: hook up icon for invalid state (e.g. bad url, bad regex, bad date).
// TODO: allow post? cookie?, at least internally.
public class VerCheck extends JApplet implements Runnable
    {
    // ------------------------------ FIELDS ------------------------------

    /**
     * applet height in pixels
     */
    private static final int APPLET_HEIGHT = 921;

    /**
     * applet width in pixels
     */
    private static final int APPLET_WIDTH = 1228;

    /**
     * column in model for URL
     */
    private static final int COL_FOR__URL = 5;
    /**
     * column in model for app name
     */
    private static final int COL_FOR_APP = 1;

    /**
     * column in model for the date released string
     */
    private static final int COL_FOR_DATE_RELEASED = 3;

    /**
     * column in model for description
     */
    private static final int COL_FOR_DESCRIPTION = 4;

    /**
     * column in model for the marker string
     */
    private static final int COL_FOR_MARKER = 6;

    /**
     * column in model for state icon
     */
    private static final int COL_FOR_STATE = 0;

    /**
     * column in model for version
     */
    private static final int COL_FOR_VERSION = 2;

    /**
     * width margin between columns in pixels
     */
    private static final int COL_MARGIN = 5;

    /**
     * width of application name column in pixels
     */
    private static final int COL_WIDTH_FOR_APP = 150;

    /**
     * width of date released column in pixels
     */
    private static final int COL_WIDTH_FOR_DATE_RELEASED = 92;
    /**
     * width of description pixels
     */
    private static final int COL_WIDTH_FOR_DESCRIPTION = 300;

    /**
     * width of marker column in pixels
     */
    private static final int COL_WIDTH_FOR_MARKER = 50;

    /**
     * width of Application state icon column in pixels
     */
    private static final int COL_WIDTH_FOR_STATE = 36;

    /**
     * width of url column in pixels
     */
    private static final int COL_WIDTH_FOR_URL = 150;

    /**
     * width of version column in pixels
     */
    private static final int COL_WIDTH_FOR_VERSION = 75;

    /**
     * initial capacity in rows of ArrayList backing the TableModel
     */
    private static final int INITIAL_ROW_CAPACITY = 100;

    /**
     * number of columns in the model, 0..6
     */
    private static final int NUMBER_OF_COLS = 7;

    /**
     * size in bytes of one AppToWatch after serialization.
     */
    private static final int SERIALIZED_SIZE = 500;

    /**
     * classes for each column, state, appname, version, date, description, url, marker,
     */
    private static final Class[] COLUMN_CLASSES =
            { ImageIcon.class, String.class, String.class, BigDate.class, String.class, URL.class, String.class, };

    /**
     * background colour, pale green to match website
     */
    private static final Color BACKGROUND_FOR_BODY = Build.BLEND_BACKGROUND;

    /**
     * when editing
     */
    private static final Color BACKGROUND_FOR_EDITING = new Color( 0xfffff0 );

    /**
     * background colour for instructions.  Default grey is too dark
     */
    private static final Color BACKGROUND_FOR_INSTRUCTIONS = new Color( 0xf8f8f8 );

    /**
     * colour to temporarily set background to when selected.  Leave foreground colour alone
     */
    private static final Color BACKGROUND_FOR_SELECTION = new Color( 0xb8cfe5/* light blue grey */ );

    /**
     * instruction background colour when submitting
     */
    private static final Color BACKGROUND_FOR_WORKING = new Color( 0x005e6e/* dark cyan */ );

    /**
     * APP colour
     */
    private static final Color FOREGROUND_FOR_APP = new Color( 0x000089 );

    /**
     * Date Released String colour
     */
    private static final Color FOREGROUND_FOR_DATE_RELEASED = new Color( 0x000089 );

    /**
     * Description colour
     */
    private static final Color FOREGROUND_FOR_DESCRIPTION = new Color( 0x000089 );

    /**
     * when editing
     */
    private static final Color FOREGROUND_FOR_EDITING = new Color( 0x000044 );

    /**
     * instruction normal color
     */
    private static final Color FOREGROUND_FOR_INSTRUCTIONS = new Color( 0x339911 );

    /**
     * Marker String colour
     */
    private static final Color FOREGROUND_FOR_MARKER = new Color( 0x000089 );

    /**
     * for titles
     */
    private static final Color FOREGROUND_FOR_TITLES = new Color( 0xdc143c );

    /**
     * URL colour
     */
    private static final Color FOREGROUND_FOR_URL = new Color( 0x442222 );

    /**
     * Version colour
     */
    private static final Color FOREGROUND_FOR_VERSION = new Color( 0xe00000 );
    /**
     * instruction colour when submitting
     */
    private static final Color FOREGROUND_FOR_WORKING = new Color( 0xccffcc/* light green */ );

    /**
     * App font
     */
    private static final Font FONT_FOR_APP = FontFactory.build( "Dialog", Font.PLAIN, 15 );

    /**
     * Date released  font
     */
    private static final Font FONT_FOR_DATE_RELEASED = FontFactory.build( "Dialog", Font.PLAIN, 14 );

    /**
     * Description font
     */
    private static final Font FONT_FOR_DESCRIPTION = FontFactory.build( "Dialog", Font.PLAIN, 14 );

    /**
     * editing font, monospaced to make mousing easier.
     */
    private static final Font FONT_FOR_EDITING = FontFactory.build( "Monospaced", Font.PLAIN, 15 );

    /**
     * instructions font
     */
    private static final Font FONT_FOR_INSTRUCTIONS = FontFactory.build( "Dialog", Font.PLAIN, 12 );

    /**
     * marker String font, monospaced to make noticing spaces easier.
     */
    private static final Font FONT_FOR_MARKER = FontFactory.build( "Monospaced", Font.PLAIN, 14 );

    /**
     * for for title second line
     */
    private static final Font FONT_FOR_TITLE2 = FontFactory.build( "Dialog", Font.PLAIN, 14 );

    /**
     * title font
     */
    private static final Font FONT_FOR_TITLES = FontFactory.build( "Dialog", Font.BOLD, 18 );
    /**
     * URL font
     */
    private static final Font FONT_FOR_URL = FontFactory.build( "Dialog", Font.PLAIN, 14 );

    /**
     * Version font
     */
    private static final Font FONT_FOR_VERSION = FontFactory.build( "Dialog", Font.PLAIN, 14 );

    /**
     * green lightning icon to check for new versions
     */
    private static final ImageIcon ICON_FOR_CHECK = new ImageIcon( VerCheck.class.getResource( "image/check.png" ) );

    /**
     * minus icon to remove a row
     */
    private static final ImageIcon ICON_FOR_MINUS = new ImageIcon( VerCheck.class.getResource( "image/minus.png" ) );

    /**
     * + icon to add a row
     */
    private static final ImageIcon ICON_FOR_PLUS = new ImageIcon( VerCheck.class.getResource( "image/plus.png" ) );

    /**
     * sub node for the app descriptions.
     */
    private static final Preferences persistedApps = Preferences.userNodeForPackage( VerCheck.class ).node( "apps" );

    /**
     * where in registry we persist our history
     */
    private static final Preferences persistence = Preferences.userNodeForPackage( VerCheck.class );

    /**
     * column headings  state, app, version, date released, url, marker
     */
    private static final String[] COL_NAMES = { "-", "App", "Version", "Released", "Description", "URL", "Marker" };
    /**
     * not displayed copyright
     */
    @SuppressWarnings( { "UnusedDeclaration" } )
    private static final String EMBEDDED_COPYRIGHT =
            "copyright (c) 2007-2008 Roedy Green, Canadian Mind Products, http://mindprod.com";

    /**
     * when this version was released
     */
    @SuppressWarnings( { "UnusedDeclaration" } )
    private static final String RELEASE_DATE = "2008-01-17";

    /**
     * title of Applet
     */
    private static final String TITLE_STRING = "VerCheck Version Change Detector";

    private static final String USUAL_INSTRUCTIONS =
            "Double click to edit; fill in fields for all the programs you want to check then click \"check for new versions\".\n"
            + "Select row to insert and click (+) to add a new application.\n"
            + "Select row to delete and click (-) to remove an application.";

    /**
     * embedded version string
     */
    @SuppressWarnings( { "UnusedDeclaration" } )
    private static final String VERSION_STRING = "3.3";

    /**
     * where JTable holds its internal data.
     */
    private final ArrayList<AppToWatch> ALL_ROWS = new ArrayList<AppToWatch>( INITIAL_ROW_CAPACITY );

    /**
     * contentPane of the JApplet
     */
    private Container contentPane;

    /**
     * about button
     */
    private JButton aboutButton;

    /**
     * button to submit URL to various sites
     */
    private JEButton checkButton;

    /**
     * button to remove currently selected app
     */
    private JEButton minusButton;

    /**
     * button to add a blank line for new app.
     */
    private JEButton plusButton;

    /**
     * title for app
     */
    private JLabel title;

    /**
     * second title line for app
     */
    private JLabel title2;

    /**
     * control scrolling of the response field
     */
    private JScrollPane scroller;

    /**
     * table of 5 columns icon,appName,url,marker,date released
     */
    @SuppressWarnings( { "FieldCanBeLocal" } )
    private JTable jTable;

    /**
     * instructions on how to use program
     */
    private JTextArea instructions;

    /**
     * SelectionModel for the jTable, controls which cell/row selected.
     */
    private ListSelectionModel selectionModel;

    /**
     * defines the table
     */
    @SuppressWarnings( { "FieldCanBeLocal" } )
    private VerCheckTableModel tableModel;

    /**
     * true if we are running as an application
     */
    private final boolean asApplication;

    // -------------------------- PUBLIC INSTANCE  METHODS --------------------------
    /**
     * constructor
     */
    public VerCheck()
        {
        this.asApplication = false;
        }

    /**
     * constructor
     *
     * @param asApplication true if running as application.
     */
    public VerCheck( boolean asApplication )
        {
        this.asApplication = asApplication;
        }

    /**
     * usual Applet init, run before start.
     */
    public void init()
        {
        Container contentPane = this.getContentPane();
        if ( !VersionCheck.isJavaVersionOK( 1, 6, 0, contentPane ) )
            {
            // effectively abort
            return;
            }
        Common13.setLaf();

        // jdk 1.5 turn on anti-alias.
        System.setProperty( "swing.aatext", "true" );
        initJTable();
        buildComponents();
        layoutFields();
        }

    /**
     * check the state of every application in the model. Run as separate thread.
     */
    public void run()
        {
        SwingUtilities.invokeLater( new Runnable()
        {
        public void run()
            {
            disableButtons();
            }
        } );
        // probe all apps to see if version has changed
        checkAllApps( false );
        checkAllApps( true  /* retry just broken links */ );
        final int rowCount = ALL_ROWS.size();

        // save results away in case app later crashes
        savePersisted();

        // completed all apps, put things back the way they were.
        final int frozenRowIndex = rowCount - 1;

        SwingUtilities.invokeLater( new

                Runnable()
                {
                public void run
                        ()
                    {
                    instructions.setText( USUAL_INSTRUCTIONS
                                          + "\n"
                                          + tableModel.getRowCount()
                                          + " applications last checked: "
                                          + AppToWatch.dateChecked.toString() );
                    instructions.setForeground( FOREGROUND_FOR_INSTRUCTIONS );
                    instructions.setBackground( BACKGROUND_FOR_INSTRUCTIONS );
                    enableButtons();
                    validate();
                    // changing instructions changes amount of room for table.
                    tableModel.ensureVisible( frozenRowIndex );
                    }
                }

        );
        }

    /**
     * probe all apps to see if version has changed
     *
     * @param justBrokens recheck just the links we count not connect to on previous pass.
     */
    private void checkAllApps( final boolean justBrokens )
        {
        // remove empty elts
        tableModel.removeEmpties();

        // put in order by appname.
        tableModel.sort();
        // no longer mark us unknown prior to pass. Leave old status is place.
        final int rowCount = ALL_ROWS.size();

        // set date now so items checked will be computed as of this date.
        AppToWatch.dateChecked = AppToWatch.localToday;

        // check website for each app in the table, reporting results as we go.
        // we don't use for:each or we would have to lock the entire table for entire run.
        for ( int rowIndex = 0; rowIndex < rowCount; rowIndex++ )
            {

            final AppToWatch row;
            final AppState state;
            synchronized ( ALL_ROWS )
                {
                row = ALL_ROWS.get( rowIndex );
                state = row.getState();
                }
            if ( !justBrokens || state == AppState.BROKENLINK )
                {
                tableModel.setState( rowIndex, AppState.CHECKING );
                final String checking = "Checking " + row.getAppName();
                final int frozenRowIndex = rowIndex;
                SwingUtilities.invokeLater( new Runnable()
                {
                public void run()
                    {
                    instructions.setText( checking );
                    instructions.setForeground( FOREGROUND_FOR_WORKING );
                    instructions.setBackground( BACKGROUND_FOR_WORKING );
                    validate();
                    tableModel.ensureVisible( frozenRowIndex );
                    }
                } );
                Thread.yield();
                Audio.CLICK.play();

                final URL url;
                final String marker;
                synchronized ( row )
                    {
                    url = row.getVersionURL();
                    marker = row.getMarker();
                    }
                if ( url != null && marker != null )
                    {
                    // read a page from the app's website
                    final Get get = new Get();
                    //  <><><><><><><><><><><><><><><><><><><><><><><><><>
                    // this is the guts, check with the website for version change.
                    String result = get.get( url, "UTF-8" );
                    int responseCode = get.getResponseCode();
                    //  <><><><><><><><><><><><><><><><><><><><><><><><><>

                    // special kludge for Microsoft that sometimes stalls if it is busy, give in one more try.
                    if ( responseCode >= 200 && result != null && result.indexOf( "<META HTTP-EQUIV=\"Refresh\"" ) > 0 )
                        {
                        try
                            {
                            Thread.sleep( 1000 );
                            }
                        catch ( InterruptedException e )
                            {
                            // nothing special
                            }
                        result = get.get( url, "UTF-8" );
                        responseCode = get.getResponseCode();
                        }

                    // analyse result
                    if ( responseCode >= 200 && result != null && result.length() > 0 )
                        {
                        boolean found;
                        if ( marker.startsWith( "regex:" ) )
                            {
                            // handle regex, string out past regex:
                            try
                                {
                                final Pattern p = Pattern.compile( marker.substring( "regex:".length() ) );
                                final Matcher m = p.matcher( result );
                                found = m.find();
                                }
                            catch ( PatternSyntaxException e )
                                {
                                Audio.INVALID_REGEX.play();
                                found = false;
                                }
                            catch ( Error e )
                                {
                                Audio.INVALID_REGEX.play();
                                found = false;
                                }
                            }
                        else
                            {
                            // handle ordinary non-regex search
                            found = result.indexOf( marker ) >= 0;
                            }
                        if ( found )
                            {
                            // state will be adjusted considering dateReleased.
                            tableModel.setState( rowIndex, AppState.UNCHANGED_RELEASED_IN_LAST_MONTH );
                            }
                        else
                            {
                            tableModel.setState( rowIndex, AppState.CHANGED );
                            Audio.NEW_VERSION.play();
                            // display info to help figure out a better marker.
                            err.println( "---- response " + responseCode + " " + get.getResponseMessage() + " ----" );
                            err.println( result );
                            err.println( "---- end response ----" );
                            }
                        }
                    else
                        {
                        tableModel.setState( rowIndex, AppState.BROKENLINK );
                        Audio.UNABLE_TO_CONNECT.play();
                        }
                    }
                else
                    {
                    // can't do the check. We don't have an URL and marker
                    tableModel.setState( rowIndex, AppState.UNKNOWN );
                    }
                }
            }// end for

        }

    /**
     * usual Applet start, run after init.
     */
    public void start()
        {
        restorePersisted();
        // override anything in the persisted state with latest settings in the Applet itself.
        restoreDefaultApps();
        if ( AppToWatch.dateChecked != null )
            {
            instructions.setText( USUAL_INSTRUCTIONS
                                  + "\nLast checked: " + AppToWatch.dateChecked.toString() );
            }
        }

    /**
     * usual Applet stop, run before destroy.
     */
    public void stop()
        {
        savePersisted();
        }

    // -------------------------- OTHER METHODS --------------------------

    /**
     * allocate and initialise all the Swing components
     */
    private void buildComponents()
        {
        contentPane = getContentPane();
        contentPane.setBackground( BACKGROUND_FOR_BODY );
        contentPane.setLayout( new GridBagLayout() );

        title = new JLabel( TITLE_STRING +
                            " " +
                            VERSION_STRING );

        title.setFont( FONT_FOR_TITLES );
        title.setForeground( FOREGROUND_FOR_TITLES );

        title2 = new JLabel(
                "released:" +
                RELEASE_DATE +
                " build:" +
                Build.BUILD_NUMBER );
        title2.setFont( FONT_FOR_TITLE2 );
        title2.setForeground( FOREGROUND_FOR_TITLES );

        // about
        aboutButton = new JEButton( "About" );
        aboutButton.setToolTipText( "About "
                                    + TITLE_STRING
                                    + " "
                                    + VERSION_STRING );
        aboutButton.addActionListener( new ActionListener()
        {
        public void actionPerformed( ActionEvent e )
            {
            // open aboutbox frame

            new CMPAboutJBox( getParentFrame(),
                    TITLE_STRING,
                    VERSION_STRING,
                    "Checks websites to see if there is a new version of a given program.",
                    "",
                    "freeware",
                    RELEASE_DATE,
                    2008,
                    "Roedy Green",
                    "VERCHECK",
                    "1.6" );
            }
        } );

        plusButton = new JEButton( "add app" );
        plusButton.setToolTipText( "Add a blank line for a new application." );
        plusButton.setIcon( ICON_FOR_PLUS );
        plusButton.addActionListener( new ActionListener()
        {
        public void actionPerformed( ActionEvent e )
            {
            // current row, or -1 if none selected.
            final int row = jTable.getSelectedRow();
            if ( row >= 0 )
                {
                // selection left pointing to new empty record.
                tableModel.add( row, new AppToWatch() );
                }
            else
                {
                // no row selected, so tack on the end.
                tableModel.add( new AppToWatch() );
                // select the new row
                final int lastRow = tableModel.getRowCount() - 1;
                selectionModel.setSelectionInterval( lastRow, lastRow );
                }
            }
        } );

        minusButton = new JEButton( "remove app" );
        minusButton.setIcon( ICON_FOR_MINUS );
        minusButton.setToolTipText( "Remove currently selected application." );
        minusButton.addActionListener( new ActionListener()
        {
        public void actionPerformed( ActionEvent e )
            {
            // current row, or -1 if none selected.
            final int rowIndex = jTable.getSelectedRow();
            if ( 0 <= rowIndex && rowIndex < tableModel.getRowCount() )
                {
                tableModel.remove( rowIndex );
                // leave selection pointing at next row that moved up.
                }
            else
                {
                // no row selected, nothing to delete,  Should not happen.
                Toolkit.getDefaultToolkit().beep();
                }
            }
        } );

        // the minus button does not work except when there is a row selected.
        selectionModel.addListSelectionListener( new ListSelectionListener()
        {
        /**
         * Called whenever the value of the selection changes.
         * @param e the event that characterizes the change.
         */
        public void valueChanged( ListSelectionEvent e )
            {
            minusButton.setEnabled( !selectionModel.isSelectionEmpty() );
            }
        } );

        checkButton = new JEButton( "check for new versions" );
        checkButton.setIcon( ICON_FOR_CHECK );
        checkButton.setToolTipText( "Check all programs to see if there is a new version" );
        checkButton.addActionListener( new ActionListener()

        {
        public void actionPerformed( ActionEvent e )
            {
            Thread t = new Thread( VerCheck.this );
            // check the apps on a separate thread
            // so Swing thread can update screen.
            t.start();// which triggers run
            }
        } );

        instructions = new JTextArea( USUAL_INSTRUCTIONS,

                4, 120 );

        instructions.setFont( FONT_FOR_INSTRUCTIONS );
        instructions.setForeground( FOREGROUND_FOR_INSTRUCTIONS );
        instructions.setBackground( BACKGROUND_FOR_INSTRUCTIONS );
        instructions.setEditable( false );
        instructions.setMargin( new Insets( 3, 3, 3, 3 ) );

        assert jTable != null : "jTable not initialised";
        // contain the response in JScrollPane.
        scroller = new JScrollPane( jTable,
                JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
                JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED );

        // control the speed effect of the wheelhouse
        scroller.getVerticalScrollBar().setUnitIncrement( 16 );
        }

    /**
     * stop user from pressing buttons
     */
    private void disableButtons()
        {
        aboutButton.setEnabled( false );
        minusButton.setEnabled( false );
        plusButton.setEnabled( false );
        checkButton.setEnabled( false );
        }

    /**
     * let user from press buttons
     */
    private void enableButtons()
        {
        aboutButton.setEnabled( true );
        minusButton.setEnabled( true );
        plusButton.setEnabled( true );
        checkButton.setEnabled( true );
        }

    /**
     * export latest information as an HTML table.
     */
    private void export()
        {
        try
            {
            // O P E N
            FileOutputStream fos = new FileOutputStream( "vercheckexport.html", false/* append */ );
            OutputStreamWriter eosw = new OutputStreamWriter( fos, "ISO-8859-1" );
            BufferedWriter bw = new BufferedWriter( eosw, 4096/* buffsize in chars */ );
            PrintWriter prw = new PrintWriter( bw, false/* auto flush on println */ );

            // W R I T E
            StringBuilder sb = new StringBuilder( 1000 );
            sb.append( "<!-- G E N E R A T E D _ B Y _ V E R C H E C K  -  D O _ N O T _ E D I T -->\n" );
            sb.append( "<table class=\"standard\" summary=\"Roedy Green&rsquo;s recommended utilities\">\n" );
            sb.append( "<thead><tr><th colspan=\"5\">Roedy&rsquo;s Recommended Utilities</th></tr>\n" );
            sb.append( "<tr><th colspan=\"5\">Last Verified " );
            sb.append( BigDate.localToday().toString() );
            sb.append( "</th></tr>\n" );
            sb.append( "<tr><th>-</th>\n" );
            sb.append( "<th>Utility</th>\n" );
            sb.append( "<th>Version</th>\n" );
            sb.append( "<th>Released</th>\n" );
            sb.append( "<th>Description</th>\n" );
            sb.append( "</tr></thead><tbody>\n" );
            prw.println( sb.toString() );

            for ( AppToWatch rowData : ALL_ROWS )
                {
                sb.setLength( 0 );
                sb.append( "<!-- G E N E R A T E D _ B Y _ V E R C H E C K  -  D O _ N O T _ E D I T -->\n" );
                sb.append( "<tr><td class=\"" );

                final AppState state = rowData.getState();
                sb.append( state.getShortName() );
                sb.append( "\"></td><td>" );
                final String downloadURL = rowData.getDownloadURL();
                if ( downloadURL.length() > 0 )
                    {
                    sb.append( "<a " );
                    if ( downloadURL.startsWith( "http://" ) )
                        {
                        sb.append( "class=\"offsite\" " );
                        }
                    sb.append( "href=\"" );

                    sb.append( rowData.getDownloadURL() );
                    sb.append( "\">" );

                    sb.append( insertHTMLEntities( rowData.getAppName() ) );
                    sb.append( "</a>" );
                    }
                else
                    {
                    sb.append( insertHTMLEntities( rowData.getAppName() ) );
                    }
                sb.append( "</td>" );
                switch ( state )
                    {
                    case CHANGED:
                    case UNCHANGED_RELEASED_IN_LAST_WEEK:
                        sb.append( "<td class=\"new\">" );
                        break;
                    default:
                        sb.append( "<td>" );
                    }

                sb.append( insertHTMLEntities( rowData.getVersion() ) );
                sb.append( "</td><td>" );
                sb.append( rowData.getDateReleased().toString() );
                sb.append( "</td><td>" );
                sb.append( insertHTMLEntities( rowData.getDescription() ) );
                sb.append( "</td></tr>" );
                prw.println( sb.toString() );
                }
            sb.append( "<!-- G E N E R A T E D _ B Y _ V E R C H E C K  -  D O _ N O T _ E D I T -->\n" );
            prw.print( "</tbody></table>" );
            prw.close();
            }

        catch ( FileNotFoundException e )
            {
            err.println( "Unable to export VerCheck data to HTML" );
            }

        catch ( UnsupportedEncodingException e )
            {
            err.println( "Unable to export VerCheck data to HTML because of lack of ISO-8859-1 support." );
            }
        }

    /**
     * find Frame/JFrame enclosing this Component. Returns null if can't find one.
     *
     * @return Frame
     */
    private Frame getParentFrame()
        {
        Container c = this.getParent();
        while ( c != null )
            {
            if ( c instanceof Frame )
                {
                return ( Frame ) c;
                }
            c = c.getParent();
            }
        return null;
        }

    /**
     * initialise the JTable of apps we monitor
     */
    private void initJTable()
        {
        tableModel = new VerCheckTableModel();

        jTable = new JTable( tableModel );
        selectionModel = jTable.getSelectionModel();

        jTable.setRowHeight( 24 );// extra vertical space for icons, and for box around edit.
        jTable.setSelectionBackground( BACKGROUND_FOR_SELECTION );

        // get information about all columns.

        final TableColumnModel columnModel = jTable.getColumnModel();

        // setting column widths:

        // state
        final TableColumn stateCol = columnModel.getColumn( COL_FOR_STATE );
        stateCol.setPreferredWidth( COL_WIDTH_FOR_STATE );
        stateCol.setMinWidth( COL_WIDTH_FOR_STATE );
        stateCol.setMaxWidth( COL_WIDTH_FOR_STATE );

        // app name
        final TableColumn appCol = columnModel.getColumn( COL_FOR_APP );
        appCol.setPreferredWidth( COL_WIDTH_FOR_APP );
        appCol.setMinWidth( 60 );
        appCol.setMaxWidth( COL_WIDTH_FOR_APP + 50 );

        // version
        final TableColumn versionCol = columnModel.getColumn( COL_FOR_VERSION );
        versionCol.setPreferredWidth( COL_WIDTH_FOR_VERSION );
        versionCol.setMinWidth( 60 );
        versionCol.setMaxWidth( COL_WIDTH_FOR_VERSION + 100 );

        // date released
        final TableColumn dateReleasedCol = columnModel.getColumn( COL_FOR_DATE_RELEASED );
        dateReleasedCol.setPreferredWidth( COL_WIDTH_FOR_DATE_RELEASED );
        dateReleasedCol.setMinWidth( 60 );
        dateReleasedCol.setMaxWidth( COL_WIDTH_FOR_DATE_RELEASED );

        // description
        final TableColumn descriptionCol = columnModel.getColumn( COL_FOR_DESCRIPTION );
        descriptionCol.setPreferredWidth( COL_WIDTH_FOR_DESCRIPTION );
        descriptionCol.setMinWidth( 60 );
        descriptionCol.setMaxWidth( COL_WIDTH_FOR_DESCRIPTION + 300 );

        // url
        final TableColumn urlCol = columnModel.getColumn( COL_FOR__URL );
        urlCol.setPreferredWidth( COL_WIDTH_FOR_URL );
        urlCol.setMinWidth( 60 );
        urlCol.setMaxWidth( COL_WIDTH_FOR_URL + 500 );

        // marker
        final TableColumn markerCol = columnModel.getColumn( COL_FOR_MARKER );
        markerCol.setPreferredWidth( COL_WIDTH_FOR_MARKER );
        markerCol.setMinWidth( 60 );
        markerCol.setMaxWidth( COL_WIDTH_FOR_MARKER + 500 );

        // adding a bit of extra space between the columns.
        columnModel.setColumnMargin( COL_MARGIN );

        // apply VerCheckHeaderRenderer to all columns.
        final VerCheckHeaderRenderer verCheckHeaderRenderer = new VerCheckHeaderRenderer();
        for ( int i = 0; i < NUMBER_OF_COLS; i++ )
            {
            columnModel.getColumn( i ).setHeaderRenderer( verCheckHeaderRenderer );
            }

        // display just an icon
        stateCol.setCellRenderer( new AppStateRenderer() );

        appCol.setCellRenderer( new RainbowRenderer( FONT_FOR_APP,
                FOREGROUND_FOR_APP,
                JLabel.LEFT ) );

        versionCol.setCellRenderer( new RainbowRenderer( FONT_FOR_VERSION,
                FOREGROUND_FOR_VERSION,
                JLabel.LEFT ) );
        dateReleasedCol.setCellRenderer( new ISODateRenderer( FONT_FOR_DATE_RELEASED,
                FOREGROUND_FOR_DATE_RELEASED,
                JLabel.LEFT ) );

        descriptionCol.setCellRenderer( new RainbowRenderer( FONT_FOR_DESCRIPTION,
                FOREGROUND_FOR_DESCRIPTION,
                JLabel.LEFT ) );

        urlCol.setCellRenderer( new RainbowRenderer( FONT_FOR_URL,
                FOREGROUND_FOR_URL,
                JLabel.LEFT ) );

        markerCol.setCellRenderer( new RainbowRenderer( FONT_FOR_MARKER,
                FOREGROUND_FOR_MARKER,
                JLabel.LEFT ) );

        // set up editors.
        final JTextField scratch = new JTextField();
        scratch.setFont( FONT_FOR_EDITING );
        scratch.setForeground( FOREGROUND_FOR_EDITING );
        scratch.setBackground( BACKGROUND_FOR_EDITING );
        final TableCellEditor cellEditor = new DefaultCellEditor( scratch );
        appCol.setCellEditor( cellEditor );
        versionCol.setCellEditor( cellEditor );
        urlCol.setCellEditor( cellEditor );
        markerCol.setCellEditor( cellEditor );
        dateReleasedCol.setCellEditor( new ISODateEditor( FOREGROUND_FOR_EDITING,
                BACKGROUND_FOR_EDITING,
                FONT_FOR_EDITING,
                JTextField.LEFT ) );

        // allow only one row to be selected at a time.
        jTable.setSelectionMode( ListSelectionModel.SINGLE_SELECTION );
        jTable.setColumnSelectionAllowed( false );
        jTable.setRowSelectionAllowed( true );
        jTable.setCellSelectionEnabled( true );
        // use default setSelectedBackground;
        }

    /**
     * layout fields using GridBagLayout
     */
    private void layoutFields()
        {
        /* 0------------------ 1 -----------2 -----3-----4-----
        0 title--------------------------title2 ------about---- 0
        1 icon_appName_ver_date_description_url_marker_(table)- 1
        2  ------------------------------(+)---(-)--check------ 2
        3 instructions----------------------------------------- 3
        ---0------------------ 1 -----------2 -----3-----4-----
        */

        // x y w h wtx wty anchor fill T L B R padx pady

        contentPane.add( title,
                new GridBagConstraints( 0,
                        0,
                        2,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.WEST,

                        GridBagConstraints.NONE,
                        new Insets( 10, 10, 5, 5 ),
                        0,
                        0 ) );
        contentPane.add( title2,
                new GridBagConstraints( 2,
                        0,
                        2,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.EAST,

                        GridBagConstraints.NONE,
                        new Insets( 10, 5, 5, 5 ),
                        0,
                        0 ) );
        contentPane.add( aboutButton,
                new GridBagConstraints( 4,
                        0,
                        1,
                        1,
                        0.0,
                        0.0,
                        GridBagConstraints.EAST,
                        GridBagConstraints.NONE,
                        new Insets( 10, 5, 5, 10 ),
                        0,
                        0 ) );

        contentPane.add( scroller
                /* contains response */,
                new GridBagConstraints( 0,
                        1,
                        5,
                        1,
                        100.0,
                        100.0,
                        GridBagConstraints.CENTER,
                        GridBagConstraints.BOTH,
                        new Insets( 5, 10, 5, 10 ),
                        0,
                        0 ) );
        contentPane.add( plusButton,
                new GridBagConstraints( 2,
                        2,
                        1,
                        1,
                        1.0,
                        0.0,
                        GridBagConstraints.EAST,
                        GridBagConstraints.NONE,
                        new Insets( 5, 5, 5, 5 ),
                        0,
                        0 ) );
        contentPane.add( minusButton,
                new GridBagConstraints( 3,
                        2,
                        1,
                        1,
                        1.0,
                        0.0,
                        GridBagConstraints.EAST,
                        GridBagConstraints.NONE,
                        new Insets( 5, 5, 5, 5 ),
                        0,
                        0 ) );

        contentPane.add( checkButton,
                new GridBagConstraints( 4,
                        2,
                        1,
                        1,
                        1.0,
                        0.0,
                        GridBagConstraints.EAST,
                        GridBagConstraints.NONE,
                        new Insets( 5, 5, 5, 10 ),
                        0,
                        0 ) );

        contentPane.add( instructions,
                new GridBagConstraints( 0,
                        3,
                        NUMBER_OF_COLS,
                        1,
                        100.0,
                        0.0,
                        GridBagConstraints.CENTER,
                        GridBagConstraints.BOTH,
                        new Insets( 5, 10, 10, 10 ),
                        0,
                        0 ) );
        }

    /**
     * restore the canned set of apps the way the program was before any data entry. Leave the user's new entries as is,
     * Revert any modified entries or deleted entries.
     */
    private void restoreDefaultApps()
        {
        for ( AppToWatch aDefaultApp : DefaultApps.DEFAULTS )
            {
            int rowIndex = tableModel.getRowForApp( aDefaultApp.getAppName() );
            if ( rowIndex < 0 )
                {
                // This default does not yet exist, add it at the end.
                tableModel.add( aDefaultApp );
                }
            else
                {
                // already exists, replace, but leaving the old status
                aDefaultApp.setState( tableModel.get( rowIndex ).getState() );
                tableModel.set( rowIndex, aDefaultApp );
                }
            }
        for ( String obsolete : DefaultApps.OBSOLETE_APPS )
            {
            // does not matter if already deleted.
            tableModel.remove( obsolete );
            }
        tableModel.sort();
        }

    /**
     * restore the table data from the registry. AllRows is already allocated.
     */
    @SuppressWarnings( "unchecked" )
    private void restorePersisted
            ()
        {
        try
            {
            tableModel.clear();
            if ( persistence.getLong( "serialVersionUID", 0 ) !=
                 AppToWatch.serialVersionUID )
                {
                err.println( "Out of date stored state." );
                tableModel.clear();
                return;
                }
            AppToWatch.dateChecked = ( new BigDate( persistence.getInt( "dateChecked", BigDate.NULL_ORDINAL ) ) );
            for ( String key : persistedApps.keys() )
                {
                // O P E N  registry
                final byte[] bai = persistedApps.getByteArray( key, null );
                if ( bai == null )
                    {
                    err.println( "existing state corrupted." );
                    tableModel.clear();
                    return;
                    }
                final ByteArrayInputStream bais = new ByteArrayInputStream( bai );
                final ObjectInputStream ois = new ObjectInputStream( bais );

                // R E A D
                tableModel.add( ( AppToWatch ) ois.readObject() );

                // C L O S E
                ois.close();
                }// end for
            }// end try
        catch ( BackingStoreException e )
            {
            err.println( "Corrupted Preferences keys." );
            tableModel.clear();
            }
        catch ( IOException e )
            {
            err.println( "no existing state to restore " + e.getMessage() );
            tableModel.clear();
            }
        catch ( ClassNotFoundException e )
            {
            err.println( "corrupted state " + e.getMessage() );
            e.printStackTrace( err );
            tableModel.clear();
            }
        }

    /**
     * save the table data in the registry.
     */
    private void savePersisted()
        {
        try
            {
            // the entire array is too big to write as one persisted field.
            // So we write app=encoded parms, one line per app
            persistence.putLong( "serialVersionUID", AppToWatch.serialVersionUID );
            persistence.putInt( "dateChecked", AppToWatch.dateChecked.getOrdinal() );
            // get rid of all previous state
            persistedApps.clear();
            for ( AppToWatch app : ALL_ROWS )
                {
                // O P E N
                final ByteArrayOutputStream baos = new ByteArrayOutputStream( SERIALIZED_SIZE );
                final ObjectOutputStream oos = new ObjectOutputStream( baos );

                // W R I T E
                oos.writeObject( app );
                final byte[] result = baos.toByteArray();

                // C L O S E
                oos.close();
                persistedApps.putByteArray( app.getAppName(), result );
                }// end for
            persistedApps.flush();
            persistence.flush();
            }
        catch ( IOException e )
            {
            err.println( "problem saving state" );
            e.printStackTrace( err );
            }
        catch ( BackingStoreException e )
            {
            err.println( "Cannot save Preferences state." );
            e.printStackTrace( err );
            }
        if ( asApplication )
            {
            /* export as HTML as well */
            export();
            }
        }

    // -------------------------- INNER CLASSES --------------------------

    private class VerCheckTableModel extends AbstractTableModel
        {
        /**
         * Replaces the rowData at the specified position in this list with the specified rowData. JTable calls its
         * methods on the Swing thread. VerCheck calls its methods on a background Thread.
         *
         * @param rowIndex rowIndex of the element to replace
         * @param rowData  rowData to be stored at the specified position
         * @return the row previously at the specified position
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        @SuppressWarnings( { "UnusedReturnValue" } )
        public AppToWatch set( final int rowIndex, final AppToWatch rowData )
            {
            final AppToWatch result;
            synchronized ( ALL_ROWS )
                {
                stopEdit();
                result = ALL_ROWS.set( rowIndex, rowData );
                }
            SwingUtilities.invokeLater( new Runnable()
            {
            public void run()
                {
                fireTableRowsUpdated( rowIndex, rowIndex );
                }
            } );
            Thread.yield();
            return result;
            }

        /**
         * Appends the specified element to the end of this list.
         *
         * @param rowData row  to be appended to this list
         * @return <tt>true</tt>
         */
        @SuppressWarnings( { "UnusedReturnValue" } )
        public boolean add( AppToWatch rowData )
            {
            final int row;
            final boolean result;
            synchronized ( ALL_ROWS )
                {
                stopEdit();
                // update the status, relative to current date
                rowData.normaliseState();
                result = ALL_ROWS.add( rowData );

                row = ALL_ROWS.size() - 1;
                selectionModel.setSelectionInterval( row, row );
                }
            SwingUtilities.invokeLater( new Runnable()
            {
            public void run()
                {
                fireTableRowsInserted( row, row );
                ensureVisible( row );
                }
            } );
            Thread.yield();
            return result;
            }

        /**
         * make sure a given rowIndex is visible
         *
         * @param rowIndex 0-based rowIndex to make sure is scrolled into view.
         */
        public void ensureVisible( final int rowIndex )
            {
            SwingUtilities.invokeLater( new Runnable()
            {
            public void run()
                {
                final Rectangle r;
                synchronized ( ALL_ROWS )
                    {
                    // automatically handle rowIndex out of range in an appropriate way
                    r = jTable.getCellRect( rowIndex, COL_FOR_STATE/* col */, true );
                    }
                if ( r != null )
                    {
                    jTable.scrollRectToVisible( r );
                    }
                }
            } );
            Thread.yield();
            }

        /**
         * sort in order by app name
         */
        public void sort()
            {
            synchronized ( ALL_ROWS )
                {
                stopEdit();
                Collections.sort( ALL_ROWS );
                selectionModel.clearSelection();
                }
            SwingUtilities.invokeLater( new Runnable()
            {
            public void run()
                {
                tableModel.fireTableDataChanged();
                }
            } );
            Thread.yield();
            }

        /**
         * remove any empty entries.
         */
        public void removeEmpties()
            {
            synchronized ( ALL_ROWS )
                {
                stopEdit();
                // must process in reverse order so numbering not jostled
                for ( int i = ALL_ROWS.size() - 1; i >= 0; i-- )
                    {
                    if ( ALL_ROWS.get( i ).getState() == AppState.EMPTY )
                        {
                        remove( i );// handles the fire
                        }
                    }
                }
            selectionModel.clearSelection();
            SwingUtilities.invokeLater( new Runnable()
            {
            public void run()
                {
                tableModel.fireTableDataChanged();
                }
            } );
            Thread.yield();
            }

        /**
         * Inserts the specified rowData at the specified position in this list. Shifts the rowData currently at that
         * position (if any) and any subsequent elements to the right (adds one to their indices).
         *
         * @param rowIndex rowIndex at which the specified row is to be inserted
         * @param rowData  rowData to be inserted
         * @throws IndexOutOfBoundsException
         */
        public void add( final int rowIndex, final AppToWatch rowData )
            {
            synchronized ( ALL_ROWS )
                {
                stopEdit();
                ALL_ROWS.add( rowIndex, rowData );
                selectionModel.setSelectionInterval( rowIndex, rowIndex );
                }
            SwingUtilities.invokeLater( new Runnable()
            {
            public void run()
                {
                fireTableRowsInserted( rowIndex, rowIndex );
                ensureVisible( rowIndex );
                }
            } );
            Thread.yield();
            }

        /**
         * abort any edit in progress
         */
        private void stopEdit()
            {
            // stop any edit in process
            TableCellEditor tce = jTable.getCellEditor();
            if ( tce != null )
                {
                tce.stopCellEditing();
                }
            }

        /**
         * search for a row matching a given application name
         *
         * @param appName name of the app to look for
         * @return row where found, -1 if not found.
         */
        public int getRowForApp( String appName )
            {
            synchronized ( ALL_ROWS )
                {
                final int rowCount = ALL_ROWS.size();
                for ( int rowIndex = 0; rowIndex < rowCount; rowIndex++ )
                    {
                    if ( ALL_ROWS.get( rowIndex ).getAppName().equalsIgnoreCase( appName ) )
                        {
                        return rowIndex;
                        }
                    }
                return -1;
                }
            }

        /**
         * get row
         *
         * @param rowIndex desired row.
         * @return row date
         */
        @SuppressWarnings( { "UnusedDeclaration" } )
        public AppToWatch get( int rowIndex )
            {
            if ( rowIndex < 0 )
                {
                return null;
                }
            else
                {
                synchronized ( ALL_ROWS )
                    {
                    return ALL_ROWS.get( rowIndex );
                    }
                }
            }

        /**
         * Removes the row at the specified position in this list. Shifts any subsequent rows to the left (subtracts one
         * from their indices).
         *
         * @param rowIndex the rowIndex of the row to be removed
         * @return the row that was removed from the list
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        @SuppressWarnings( { "UnusedReturnValue" } )
        public AppToWatch remove( final int rowIndex )
            {
            final AppToWatch result;
            synchronized ( ALL_ROWS )
                {
                result = ALL_ROWS.remove( rowIndex );
                // item below pops up to become the new selection
                if ( rowIndex < ALL_ROWS.size() )
                    {
                    selectionModel.setSelectionInterval( rowIndex, rowIndex );
                    }
                else
                    {
                    selectionModel.clearSelection();
                    }
                }
            SwingUtilities.invokeLater( new Runnable()
            {
            public void run()
                {
                fireTableRowsDeleted( rowIndex, rowIndex );
                }
            } );
            Thread.yield();
            return result;
            }

        /**
         * Removes the row with specified appname. Shifts any subsequent rows to the left (subtracts one from their
         * indices).
         *
         * @param appName the name of the app to be removed
         * @return the row that was removed from the list
         * @throws IndexOutOfBoundsException {@inheritDoc}
         */
        @SuppressWarnings( { "UnusedReturnValue" } )
        AppToWatch remove( final String appName )
            {
            final int rowIndex = getRowForApp( appName );
            return rowIndex >= 0 ? remove( rowIndex ) : null;
            }

        /**
         * Removes all entries from the model
         */
        @SuppressWarnings( { "UnusedDeclaration" } )
        public void clear()
            {
            synchronized ( ALL_ROWS )
                {
                ALL_ROWS.clear();
                selectionModel.clearSelection();
                }
            SwingUtilities.invokeLater( new Runnable()
            {
            public void run()
                {
                tableModel.fireTableDataChanged();
                }
            } );
            Thread.yield();
            }

        /**
         * Returns class of column.
         *
         * @param columnIndex the column being queried
         * @return the Object.class
         */
        public Class<?> getColumnClass( int columnIndex )
            {
            return COLUMN_CLASSES[ columnIndex ];
            }

        /**
         * Can you edit this cell
         *
         * @param rowIndex    the row being queried
         * @param columnIndex the column being queried
         * @return false
         */
        public boolean isCellEditable( int rowIndex, int columnIndex )
            {
            return columnIndex != COL_FOR_STATE;
            }

        /**
         * set the application state of a rowIndex
         *
         * @param rowIndex 0-based rowIndex number
         * @param state    Application state
         */
        public void setState( final int rowIndex, AppState state )
            {
            synchronized ( ALL_ROWS )
                {
                ALL_ROWS.get( rowIndex ).setState( state );
                }
            SwingUtilities.invokeLater( new Runnable()
            {
            public void run()
                {
                tableModel.fireTableCellUpdated( rowIndex, COL_FOR_STATE/* col */ );
                }
            } );
            Thread.yield();
            }

        /**
         * Return columnIndex names
         *
         * @param columnIndex the columnIndex being queried
         * @return a string containing the default name of <code>columnIndex</code>
         */
        public String getColumnName( int columnIndex )
            {
            return COL_NAMES[ columnIndex ];
            }

        /**
         * how many rows are there?
         *
         * @return number of rows.
         */
        public int getRowCount()
            {
            synchronized ( ALL_ROWS )
                {
                return ALL_ROWS.size();
                }
            }

        /**
         * how many columns are there?
         *
         * @return number of columns.
         */
        public int getColumnCount()
            {
            return NUMBER_OF_COLS;
            }

        /**
         * get cell at row/col.
         *
         * @param rowIndex    0-based row
         * @param columnIndex 0-based col
         * @return item at row/col.
         */
        public Object getValueAt( int rowIndex, int columnIndex )
            {
            synchronized ( ALL_ROWS )
                {
                final AppToWatch rowData = ALL_ROWS.get( rowIndex );
                switch ( columnIndex )
                    {
                    case COL_FOR_STATE:
                        return rowData.getIcon();
                    case COL_FOR_APP:
                        return rowData.getAppName();
                    case COL_FOR_VERSION:
                        return rowData.getVersion();
                    case COL_FOR_DATE_RELEASED:
                        return rowData.getDateReleased();
                    case COL_FOR_DESCRIPTION:
                        return rowData.getDescription();
                    case COL_FOR__URL:
                        return rowData.getVersionURL();
                    case COL_FOR_MARKER:
                        return rowData.getMarker();

                    default:
                        return null;
                    }
                }
            }

        /**
         * This empty implementation is provided so users don't have to implement this method if their data model is not
         * editable.
         *
         * @param aValue      value to assign to cell
         * @param rowIndex    row of cell
         * @param columnIndex column of cell
         */
        public void setValueAt( Object aValue, int rowIndex, int columnIndex )
            {
            synchronized ( ALL_ROWS )
                {
                final AppToWatch rowData = ALL_ROWS.get( rowIndex );
                switch ( columnIndex )
                    {
                    case COL_FOR_STATE:
                        assert false : "attempt to set state via setValueAt";
                        break;

                    case COL_FOR_APP:
                        rowData.setAppName( ( String ) aValue );
                        break;

                    case COL_FOR_VERSION:
                        rowData.setVersion( ( String ) aValue );
                        break;

                    case COL_FOR_DATE_RELEASED:
                        rowData.setDateReleased( ( BigDate ) aValue );
                        break;

                    case COL_FOR_DESCRIPTION:
                        rowData.setDescription( ( String ) aValue );
                        break;

                    case COL_FOR__URL:
                        try
                            {
                            rowData.setVersionURL( new URL( ( String ) aValue ) );
                            }
                        catch ( MalformedURLException e )
                            {
                            err.println( "invalid URL: " + aValue.toString() );
                            Audio.BAD_URL.play();
                            }
                        break;

                    case COL_FOR_MARKER:
                        rowData.setMarker( ( String ) aValue );
                        break;

                    default:
                        // ignore
                    }
                }
            }
        }

    // --------------------------- main() method ---------------------------

    /**
     * Allow this applet to run as as application as well.
     *
     * @param args command line arguments ignored.
     */
    public static void main( String args[] )
        {
        // when run as application will call init, start, stop, destroy
        HybridJ.fireup( new VerCheck( true/* as application */ ),
                TITLE_STRING + " " + VERSION_STRING,
                APPLET_WIDTH,
                APPLET_HEIGHT );
        }// end main
    }
