001/* A program to edit sqlite3 databases
002 *
003 * Copyright (C) 2015 Sidney Marshall (swm@cs.rit.edu)
004 *
005 * This program is free software: you can redistribute it and/or
006 * modify it under the terms of the GNU General Public License as
007 * published by the Free Software Foundation, either version 3 of the
008 * License, or (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but
011 * WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013 * General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program.  If not, see
017 * <http://www.gnu.org/licenses/>.
018 */
019
020//import javax.swing.ListCellRenderer;
021//import javax.swing.filechooser.FileFilter;
022
023import java.awt.BorderLayout;
024import java.awt.Color;
025import java.awt.Component;
026import java.awt.Dimension;
027import java.awt.GridLayout;
028import java.awt.Point;
029import java.awt.Rectangle;
030import java.awt.Toolkit;
031import java.awt.datatransfer.DataFlavor;
032import java.awt.datatransfer.StringSelection;
033import java.awt.datatransfer.Transferable;
034import java.awt.datatransfer.UnsupportedFlavorException;
035import java.awt.event.ActionEvent;
036import java.awt.event.ActionListener;
037import java.awt.event.KeyEvent;
038import java.awt.event.MouseAdapter;
039import java.awt.event.MouseEvent;
040import java.awt.event.WindowAdapter;
041import java.awt.event.WindowEvent;
042import java.awt.print.PrinterException;
043import java.io.Closeable;
044import java.io.File;
045import java.io.FileInputStream;
046import java.io.FileOutputStream;
047import java.io.IOException;
048import java.io.OutputStream;
049import java.nio.charset.Charset;
050import java.sql.Connection;
051import java.sql.DriverManager;
052import java.sql.PreparedStatement;
053import java.sql.ResultSet;
054import java.sql.ResultSetMetaData;
055import java.sql.SQLException;
056import java.sql.Statement;
057import java.text.MessageFormat;
058import java.util.ArrayList;
059import java.util.Arrays;
060import java.util.Collections;
061import java.util.List;
062import javax.swing.AbstractAction;
063import javax.swing.ActionMap;
064import javax.swing.Box;
065import javax.swing.BoxLayout;
066import javax.swing.DefaultComboBoxModel;
067import javax.swing.DefaultListCellRenderer;
068import javax.swing.DropMode;
069import javax.swing.InputMap;
070import javax.swing.JButton;
071import javax.swing.JCheckBox;
072import javax.swing.JComboBox;
073import javax.swing.JComponent;
074import javax.swing.JFileChooser;
075import javax.swing.JFrame;
076import javax.swing.JLabel;
077import javax.swing.JList;
078import javax.swing.JPanel;
079import javax.swing.JScrollPane;
080import javax.swing.JTable;
081import javax.swing.JTextArea;
082import javax.swing.JTextField;
083import javax.swing.KeyStroke;
084import javax.swing.ListSelectionModel;
085import javax.swing.ScrollPaneConstants;
086import javax.swing.SwingConstants;
087import javax.swing.SwingUtilities;
088import javax.swing.TransferHandler;
089import javax.swing.border.EmptyBorder;
090import javax.swing.event.DocumentEvent;
091import javax.swing.event.DocumentListener;
092import javax.swing.event.UndoableEditEvent;
093import javax.swing.event.UndoableEditListener;
094import javax.swing.filechooser.FileNameExtensionFilter;
095import javax.swing.table.AbstractTableModel;
096import javax.swing.table.DefaultTableCellRenderer;
097import javax.swing.table.TableCellEditor;
098import javax.swing.table.TableColumn;
099import javax.swing.text.Caret;
100import javax.swing.text.Document;
101import javax.swing.undo.CannotUndoException;
102import javax.swing.undo.UndoManager;
103
104// Note that 'xxx' is a string and "xxx" is an identifier in SQL.
105// could fix this by using illegal character as db/table separator.
106
107/**
108 * This class implements a window displaying the tables and views
109 * contained in a database (or all tables and views in all attached
110 * databases).
111 *
112 * Left clicking on a table name will bring up a table editor. Right
113 * clicking on a table name will bring up a find/replace window that
114 * operates on tables. There are four buttons:
115 *   Refresh - refreshes the window contents (needed after adding tables)
116 *   Show Hidden Tables - shows sqlite tables and name of database
117 *   Open Command Window - opens a command window for sql statements
118 *   Open Database - opens a database in a new process
119 *
120 * Types of windows:
121 *   database chooser - chooses a database file to edit or view
122 *   database editor - list of tables and views in database
123 *   table editor - contents of a database table or view or result set
124 *   result table viewer - a view on a select statement result
125 *   cell editor - editor for a cell in a table, view, or result set
126 *   command window - a window for executing sql commands
127 *   find/replace - controls finding and replacing on a cell editor
128 *   column search window - searches a table column
129 *
130 * All internal operations oprate on strings. When accessing the
131 * database the dbCharset is used for translation to and from the
132 * database to Strings (the second argument on the command line
133 * defaulting to UTF-8). When accessing files the fileCharset is used
134 * to convert file bytes to and from Strings (the third argument on
135 * the command line defaulting to UTF-8).
136 */
137public class DatabaseEditor extends JFrame {
138  final static long serialVersionUID = 42;
139
140  Connection conn; // the database connection
141  ArrayList<JFrame> dependents = new ArrayList<JFrame>();
142  //static Charset charset = Charset.availableCharsets().get("windows-1252");
143  //static Charset charset = Charset.availableCharsets().get("UTF-8");
144  // null for default character set
145  static Charset dbCharset = Charset.availableCharsets().get("UTF-8"); // use (UTF-8)
146  static Charset fileCharset = Charset.availableCharsets().get("UTF-8"); // use (UTF-8)
147
148  /**
149   * Closes a closable closable file/object ignoring null arguments
150   * and handling exceptions.
151   *
152   * @param file the file/object to be closed
153   */
154  static void close(Closeable file) {
155    try {
156      if(file != null) file.close();
157    } catch(IOException e) {
158      e.printStackTrace();
159    }
160  }
161
162  /**
163   * Gets an item from a ResultSet using the statically defined
164   * character set.
165   *
166   * @param rs the result set to use
167   * @param i the column number to fetch
168   * @return the result string or null
169   * @throws SQLException Throws if problem with SQLite.
170   */
171  String getWithCharset(ResultSet rs, int i) throws SQLException {
172    //if(dbCharset == null) return rs.getString(i);
173    byte[] data = rs.getBytes(i);
174    if(data == null) return rs.getString(i); // to return possible empty string
175    return new String(data, dbCharset);
176  }
177
178  /**
179   * Sets a ? item in a PreparedStatement from a string using the java
180   * default character set
181   *
182   * This method originally created blobs created by setBytes causing
183   * problems when comparing with numerical values. The commented out
184   * lines exhibit this behavior. The current setString method does
185   * not create blobs. We probably should check and use Strings for
186   * numbers and blobs for non-numbers.
187   *
188   * Could cast the ? to a TEXT via "cast(? as text)"
189   *
190   * @param stat the prepared statement with ?
191   * @param i the index of the ? to be set
192   * @param data the string of data or null
193   * @throws SQLException thrown if problems with SQLite
194   */
195  void putWithCharset(PreparedStatement stat, int i, String data) throws SQLException {
196    if(data == null) {
197      stat.setNull(i, java.sql.Types.NULL);
198    } else {
199      //byte[] raw = dbCharset == null ? data.getBytes() : data.getBytes(dbCharset);
200      //stat.setBytes(i, raw);
201      stat.setString(i, data);
202    }
203  }
204
205  static final int BUFSIZ = 4096; // used for buffer size for reading
206
207  /**
208   * Read an entire file into a String using fileCharset for
209   * conversion. Note that the conversion must be all at once because
210   * characters may straddle buffer boundaries. Returns null on error.
211   *
212   * @param file the name of the file to be read in
213   * @return the entire contents of the file converted to a String
214   */
215  String readFileIntoString(File file) {
216    FileInputStream in = null;
217    int totalRead = 0;
218    int numRead;
219    byte[] bytearray = null;
220    byte[] buffer = new byte[BUFSIZ];
221    try {
222      in = new FileInputStream(file);
223      ArrayList<byte[]> buffers = new ArrayList<byte[]>();
224      while((numRead = in.read(buffer, 0, BUFSIZ)) > 0) {
225        if(numRead != BUFSIZ) {
226          byte[] temp = new byte[numRead];
227          System.arraycopy(buffer, 0, temp, 0, numRead);
228          buffer = temp;
229        }
230        buffers.add(buffer);
231        buffer = new byte[BUFSIZ];
232        totalRead += numRead;
233      }
234      bytearray = new byte[totalRead];
235      int bufPointer = 0;
236      for(byte[] array : buffers) {
237        int size = array.length;
238        System.arraycopy(array, 0, bytearray, bufPointer, size);
239        bufPointer += size;
240      }
241    } catch(IOException ex) {
242      ex.printStackTrace();
243    } finally {
244      close(in);
245    }
246    return new String(bytearray, fileCharset);
247  }
248
249  /**
250   * if null return "null" otherwise surround string with the quote
251   * character and escape quote characters with a double quote
252   * character. This will allow using strings in SQL statements and
253   * double quotes for identifiers.
254   *
255   * We now use ? in sql for data - this handles nulls too.
256   *
257   * @param s the String to escape
258   * @param quote the quote character
259   * @return the escaped string
260   */
261  static String escapeString(String s, char quote) {
262    if(s == null) return "null";
263    String q1 = Character.toString(quote);
264    String q2 = q1 + q1;
265    return q1 + s.replace(q1, q2) + q1;
266  }
267
268  /**
269   * Finds String in another String ignoring case if ignoreCase is
270   * true. If s1 is null, returns -1. Search starts at fromIndex. To
271   * search the entire string set fromIndex to 0.
272   *
273   * @param ignoreCase true if case is to be ignored
274   * @param s1 String to search
275   * @param s2 String to search for
276   * @param fromIndex location to start search
277   * @return index of s2 in s1 or -1 if unsuccessful
278   */
279  int findIgnoreCase(boolean ignoreCase, String s1, String s2, int fromIndex) {
280    if(s2.length() == 0) return -1;
281    if(s1 == null) return -1;
282    if(ignoreCase) {
283      int length = s2.length();
284      for(int i = fromIndex; i <= s1.length() - length; ++i) {
285        if(s1.substring(i, i + length).equalsIgnoreCase(s2)) {
286          return i;
287        }
288      }
289      return -1;
290    }
291    return s1.indexOf(s2, fromIndex);
292  }
293
294  /**
295   * An undoable text edit window
296   *
297   * This is the base class for all of the text windows. Since the
298   * layout is BorderLayout (the default) clients can add buttons or
299   * other things around the text window. The variable textArea can be
300   * used to add listeners to the text area. The number of undos or
301   * redos is unlimited.
302   */
303  class EditWindow extends JFrame {
304    final static long serialVersionUID = 42;
305
306    ArrayList<JFrame> dependents = new ArrayList<JFrame>();
307    JTextArea textArea;
308
309    /**
310     * Make a text edit window with given title and
311     * contents. Dependent windows will be closed. The caller is
312     * responsible for displaying the window and adding any
313     * appropriate listeners.
314     *
315     * @param title the title of the window
316     * @param contents the initial contents of the window
317     * @param parent the JFrame on which this window is dependent.
318     */
319    EditWindow(String title, String contents, JFrame parent) {
320      super(title);
321      addWindowListener(new WindowAdapter() {
322          public void windowClosing(WindowEvent e) { EditWindow.this.dispose(); }
323          public void windowClosed(WindowEvent e) {
324            for(JFrame d : dependents) d.dispose();
325            if(parent instanceof Table) {
326              Table p = (Table)parent;
327              if(p.dependents.remove(EditWindow.this)) {
328              } else if(DatabaseEditor.this.dependents.remove(EditWindow.this)) {
329              } else {
330                System.out.println("Can't find#1: " + EditWindow.this.getTitle());
331              }
332            } else if(parent instanceof DatabaseEditor) {
333              DatabaseEditor p = (DatabaseEditor)parent;
334              if(p.dependents.remove(EditWindow.this)) {
335              } else {
336                System.out.println("Can't find#2: " + EditWindow.this.getTitle());
337              }
338            } else {
339              System.out.println("bad parent: " + parent);
340            }
341          }});
342      textArea = new UndoableTextArea(contents);
343      textArea.setLineWrap(true);
344      textArea.setWrapStyleWord(true);
345      textArea.addMouseListener(new MouseAdapter() {
346          public void mouseClicked(MouseEvent e) {
347            if(e.getButton() == 3) {
348              JFrame findReplace = new FindReplace(EditWindow.this, true);
349              EditWindow.this.dependents.add(findReplace);
350              findReplace.pack();
351              findReplace.setVisible(true);
352            }
353          }
354        });
355      JScrollPane areaScrollPane = new JScrollPane(textArea);
356      setPreferredSize(new Dimension(600, 300));
357      getContentPane().add(areaScrollPane);
358    }
359
360    /**
361     * Wrap lines in text that are too long. Insert prefix before broken
362     * lines. Delete (some) trailing whitespace
363     *
364     * @param text The text to wrap lines in.
365     * @param maxLineLength the maximum line length desired
366     * @param prefix the prefix for lines that had to be split
367     * @return a String with all lines wrapped
368     */
369    String wrapLines(String text, int maxLineLength, String prefix) {
370      int maxLength = maxLineLength;
371      int shortLength = maxLineLength - prefix.length();
372      StringBuilder newText = new StringBuilder();
373      //String text = textArea.getText();
374      int textLength = text.length();
375      while(textLength > 0 && (text.charAt(textLength - 1) == '\n' ||
376                               text.charAt(textLength - 1) == ' ')) {
377        --textLength;
378      }
379      text = text.substring(0, textLength) + '\n';
380      int end;
381
382      for(int start = 0 ; start < textLength ; start = end) {
383        end = text.indexOf('\n', start) + 1;
384        if(end == -1) end = textLength;
385        int lineLength = end - start - 1;
386        if(lineLength <= maxLength) {
387          newText.append(text.substring(start, end));
388          maxLength = maxLineLength;
389        } else {
390          end = text.lastIndexOf(' ', start + maxLength);
391          if(end <= start) end = start + maxLength;
392          newText.append(text.substring(start, end));
393          newText.append('\n');
394          newText.append(prefix);
395          maxLength = shortLength;
396        }
397      }
398      return newText.toString();
399    }
400
401    /**
402     * Break lines in paragaphs respecting the maxLength parameter.
403     *
404     * start processing =&gt;
405     *  get indent, line length, skip whitespace
406     *  set NLin
407     *
408     * letter: add to word
409     *
410     * space: if word != ""
411     *
412     * generally maxLength = 70
413     *
414     * @param maxLength the maximum line length desired
415     */
416    void breakLines(int maxLength) {
417      String text = textArea.getText();
418      int start = textArea.getSelectionStart();
419      int end = textArea.getSelectionEnd();
420      // Find start of current line - OK if it is -1
421      int in = text.lastIndexOf('\n', start - 1);
422      int lineLength = start - in - 1;
423      StringBuilder newText = new StringBuilder();
424      StringBuilder indent = new StringBuilder();
425      int NLin = 0; // number of consecutive NLs
426      // set first indent
427      char c = 0;
428      while(++in < end && (c = text.charAt(in)) <= ' ') {
429        if(c == '\n') {
430          //lineLength = maxLength;
431          indent.setLength(0);
432          NLin = 1;
433          //newText.append('\n');
434          newText.append(indent);
435          //newText.append("xxxxx");
436          break;
437        } else {
438          indent.append(c);
439          if(in >= start) {
440            ++lineLength;
441          }
442        }
443      }
444      // first indent is now set
445      // handle first word at beginning of selection
446      in = start;
447      // don't touch if start to middle of word
448      // or if not at end of word
449      // if start in indent
450      while(in < end) {
451        c = text.charAt(in);
452        if(c > ' ') {
453          newText.append(c);
454          ++lineLength;
455          ++in;
456        } else break;
457      }
458      StringBuilder word = new StringBuilder();
459      /*
460       * State
461       * in - in into input string
462       * lineLength - length of current line
463       *            - maxLength if forcing a new line
464       * word - current word being built
465       * newText - replacement text being built
466       * indent - current indentation string
467       * NLin - number of NLs in whitespace
468       *
469       */
470      //System.out.println("------------------");
471      while(in < end) {
472        /*
473          System.out.println("in: " + in +
474          " lineLength: " + lineLength +
475          " word: |" + word +
476          "| newText: |" + newText +
477          "| indent: |" + indent +
478          "| NLin: " + NLin);
479        */
480        c = text.charAt(in++);
481        if(c > ' ') {
482          word.append(c);
483        } else {
484          if(word.length() > 0) {
485            int newLength
486              = lineLength + word.length() + 1;
487            if(newLength < maxLength) {
488              newText.append(' ');
489              lineLength = newLength;
490            } else {
491              newText.append('\n');
492              newText.append(indent);
493              lineLength = indent.length() + word.length();
494            }
495            newText.append(word);
496            word.setLength(0);
497            NLin = 0;
498          }
499          if(c == '\n') {
500            ++NLin;
501            if(NLin > 1) {
502              indent.setLength(0);
503              lineLength = maxLength;
504              newText.append('\n');
505            }
506          } else if(NLin > 1) {
507            indent.append(c);
508          }
509        }
510      }
511      // need to cleanup last word
512      // should get length of word after end
513      // preserve space after last word
514      if(word.length() > 0) {
515        int wl = word.length();
516        for(int i = end; i < text.length(); i++) {
517          if(text.charAt(i) > ' ') {
518            ++wl;
519          } else break;
520        }
521        int newLength = lineLength + wl + 1;
522        if(newLength < maxLength) {
523          newText.append(' ');
524          lineLength = newLength;
525        } else {
526          newText.append('\n');
527          newText.append(indent);
528        }
529        newText.append(word);
530        if(c == '\n') newText.append(c);
531        else if(c <= ' ') newText.append(' ');
532      } else {
533        // add ' ' if ???
534      }
535      textArea.replaceSelection(newText.toString());
536      end = textArea.getSelectionEnd();
537      textArea.select(end - newText.length(), end);
538      textArea.grabFocus(); // to show current selection
539    }
540
541    /**
542     * Print the contents of the EditWindow.
543     * //////////// should be in separate thread.
544     *
545     * @param title The title on each page
546     */
547    void print(String title) {
548      MessageFormat footer = new MessageFormat("Page - {0}");
549      try {
550        JTextArea ta = new JTextArea(wrapLines(textArea.getText(), 80, ">>>>") + "\n "); // needed to print last line
551        ta.print(new MessageFormat(title), footer);
552      } catch(PrinterException e) {
553        System.out.println(e);
554      }
555    }
556  } // class EditWindow extends JFrame
557
558  /**
559   * This class implements a window displaying the contents of a
560   * database table, view, or result set. The first constructor
561   * displays tables and views and the second constructor displays
562   * result sets.
563   */
564  class Table extends JFrame {
565    final static long serialVersionUID = 42;
566
567    String tableName; // the name of the table (and title of window)
568    ArrayList<JFrame> dependents = new ArrayList<JFrame>();
569    JTable table;
570    ArrayList<String> heading;
571    ArrayList<ArrayList<String>> data; // row of columns
572    ArrayList<ArrayList<String>> oldData; // original data saved for possible restore
573    ArrayList<String> rowid; // _rowid_ for each row for updating
574
575    /**
576     * Initialize tables for a table or view with null argument.
577     * Initialize tables for a result set with result set argument.
578     * If called with a null argument then the variable tableName is used
579     * as the table to view.
580     *
581     * Assume that names of databases and tables do not contain "."
582     *
583     * @param rs the result set to view or null if a table or view
584     */
585    void initTables(ResultSet rs) {
586      boolean flag = rs == null; // true if table; false if result set
587      heading = new ArrayList<String>();
588      data = new ArrayList<ArrayList<String>>();
589      oldData = new ArrayList<ArrayList<String>>();
590      rowid = new ArrayList<String>();
591      Statement stat = null;
592      try {
593        if(flag) {
594          stat = conn.createStatement();
595          //views no longer have a _rowid_ so must check
596          try {
597            rs = stat.executeQuery("select _rowid_,* from \""
598                                   + tableName.replace(".", "\".\"")
599                                   + "\";");
600          } catch(SQLException ex) {
601            rs = stat.executeQuery("select null as _rowid_,* from \""
602                                   + tableName.replace(".", "\".\"")
603                                   + "\";");
604          }
605        }
606        ResultSetMetaData md = rs.getMetaData();
607        int n = md.getColumnCount();
608
609        for(int i = (flag ? 2 : 1); i <= n; i++) {
610          heading.add(md.getColumnName(i));
611        }
612        while (rs.next()) {
613          rowid.add(flag ? rs.getString(1) : null);
614          ArrayList<String> row = new ArrayList<String>();
615          ArrayList<String> oldRow = new ArrayList<String>();
616          for(int i = (flag ? 2 : 1); i <= n; i++) {
617            String s = getWithCharset(rs, i);
618            row.add(s);
619            oldRow.add(s);
620          }
621          data.add(row);
622          oldData.add(oldRow);
623        }
624      } catch(SQLException ex) {
625        ex.printStackTrace();
626      } finally {
627        try {
628          if(stat != null) stat.close();
629        } catch(SQLException closeex) {
630          closeex.printStackTrace();
631        }
632      }
633    }
634
635    /**
636     * Compare two Strings for equality. Also handles nulls.
637     *
638     * @param x first string
639     * @param y second string
640     * @return true if strings are equal or both are null
641     */
642    boolean eql(String x, String y) {
643      if(x == y) return true;
644      if(x == null || y == null) return false;
645      return x.equals(y);
646    }
647
648    /**
649     * This TableModel gets data from and stores data into the
650     * containing Table class.  The model allows drag &amp; drop and
651     * cell editing. The default editor is disabled if there is a
652     * new-line in the string.
653     */
654    AbstractTableModel tableModel = new AbstractTableModel() {
655        final static long serialVersionUID = 42;
656
657        public int getColumnCount() {
658          return heading.size();
659        }
660
661        public int getRowCount() {
662          return data.size();
663        }
664
665        public String getColumnName(int col) {
666          return heading.get(col);
667        }
668
669        public Object getValueAt(int row, int col) {
670          return data.get(row).get(col);
671        }
672
673        /*
674         * JTable uses this method to determine the default renderer/
675         * editor for each cell. If we didn't implement this method then
676         * the editor might not be a String editor. I always use
677         * Strings.
678         */
679        public Class<?> getColumnClass(int c) {
680          return String.class;
681        }
682
683        /**
684         * Disable the default editor if the cell contains a new line as
685         * the default editor is not reliable in this case.
686         */
687        public boolean isCellEditable(int row, int col) {
688          String value = (String)tableModel.getValueAt(row, col);
689          return value == null || value.indexOf('\n') < 0;
690        }
691
692        public void setValueAt(Object value, int row, int col) {
693          if(row < data.size()) {
694            String current = data.get(row).get(col);
695            if(!eql((String)value, current)) {
696              data.get(row).set(col, (String)value);
697              fireTableCellUpdated(row, col);
698            }
699          }
700        }
701      };
702
703    /**
704     * Create an edit window on a Table cell and update table focus to
705     * edited cell. Caller is responsible for setting location and
706     * showing the window. Don't search for an existing cellEditWindow
707     * if rowid is null.
708     *
709     * @param jtable the table containing the cell to be edited
710     * @param i the row of the Table being edited
711     * @param jj the column of the Table being edited
712     * @param location the location to put the CellEditWindow
713     * @return returns the new EditWindow
714     */
715    EditWindow makeCellEditWindow(Table jtable, int i, int jj, Rectangle location) {
716      table.changeSelection(i, jj, false, false); // update focus
717      int j = jtable.table.convertColumnIndexToModel(jj);
718      String id = rowid.get(i); // used in case rows are deleted
719      String title = tableName + "." + heading.get(j)
720        + "[" + id + "]";
721      if(id != null) {
722        for(JFrame jf : jtable.dependents) {
723          if(jf.getTitle().equals(title)) {
724            EditWindow editw = (EditWindow)jf;
725            editw.setExtendedState(JFrame.NORMAL);
726            editw.setVisible(true);
727            return editw;
728          }
729        }
730      }
731      EditWindow editWindow = new EditWindow(title, (String)tableModel.getValueAt(i, j), jtable);
732      JTextArea textArea = editWindow.textArea;
733      DocumentListener documentListener = new DocumentListener() {
734          Color color = Color.white;
735
736          void checkForChanges() {
737            String window = textArea.getText();
738            String table = (String)tableModel.getValueAt(i, j);
739            Color temp = eql(window, table) ? Color.white : Color.pink;
740            if(!color.equals(temp)) {
741              textArea.setBackground(temp);
742              color = temp;
743            }
744          }
745
746          public void changedUpdate(DocumentEvent e) {
747            checkForChanges();
748          };
749
750          public void insertUpdate(DocumentEvent e) {
751            checkForChanges();
752          };
753
754          public void removeUpdate(DocumentEvent e) {
755            checkForChanges();
756          };
757        };
758      textArea.getDocument().addDocumentListener(documentListener);
759      JPanel btns = new JPanel(new GridLayout(id != null ? 4 : 3, 1));
760      btns.add(new JButton("Open in Command Window") {
761          final static long serialVersionUID = 42;
762
763          {
764            addActionListener(new ActionListener() {
765                public void actionPerformed(ActionEvent e) {
766                  textArea.grabFocus(); // to show current selection
767                  makeCommandWindow("Data Command Window",
768                                    textArea.getText());
769                }
770              });
771          }
772        });
773      btns.add(new JButton("Break Lines in Selection") {
774          final static long serialVersionUID = 42;
775
776          {
777            addActionListener(new ActionListener() {
778                public void actionPerformed(ActionEvent e) {
779                  editWindow.breakLines(70);
780                }
781              });
782          }
783        });
784      btns.add(new JButton("Print") {
785          final static long serialVersionUID = 42;
786
787          {
788            addActionListener(new ActionListener() {
789                public void actionPerformed(ActionEvent e) {
790                  editWindow.print(editWindow.getTitle());
791                }
792              });
793          }
794        });
795      if(id != null) {
796        btns.add(new JButton("Update") {
797            final static long serialVersionUID = 42;
798
799            {
800              addActionListener(new ActionListener() {
801                  public void actionPerformed(ActionEvent e) {
802                    String s = textArea.getText();
803                    int newi = rowid.indexOf(id);
804                    if(newi >= 0) {
805                      tableModel.setValueAt(s, newi, j);
806                      documentListener.changedUpdate(null);
807                    }
808                    textArea.grabFocus(); // to show current selection
809                  }
810                });
811            }
812          });
813      }
814      editWindow.getContentPane().add(btns, BorderLayout.SOUTH);
815      if(id != null) Table.this.dependents.add(editWindow); // if result set then keep longer
816      else DatabaseEditor.this.dependents.add(editWindow); // if table or view then depend on table
817      if(location != null) {
818        editWindow.setLocation(location.x, location.y + location.height);
819        editWindow.pack();
820        editWindow.setVisible(true);
821      } else {
822        editWindow.setLocationByPlatform(true);
823        editWindow.pack();
824        editWindow.setVisible(true);
825      }
826      return editWindow;
827    }
828
829    /**
830     * This is the JPanel displaying the database table, view, or
831     * result set.  Left clicking on a cell will select it. Double
832     * clicking on a cell will enable the cell editor if the cell does
833     * not contain new lines. Right clicking on a cell will bring up a
834     * cell editor window. Right clicking on a column label will bring
835     * up a find/replace window on the column.
836     */
837    class DBTable extends JPanel {
838      final static long serialVersionUID = 42;
839
840      /**
841       * Constructor for displaying a table, view, or ResultSet. With
842       * a null argument this will display the table names in the
843       * variable tableName.
844       *
845       * @param rs ResultSet to display or null if table of view
846       */
847      public DBTable(ResultSet rs) {
848        super(new GridLayout(1,0));
849
850        setOpaque(true);
851
852        initTables(rs);
853
854        table = new JTable(tableModel);
855        table.setAutoCreateColumnsFromModel(true);
856        table.setPreferredScrollableViewportSize(new Dimension(500, 70));
857        table.setFillsViewportHeight(true);
858        table.setRowSelectionAllowed(false);
859        table.setColumnSelectionAllowed(false);
860        table.setCellSelectionEnabled(true);
861        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
862        table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
863        table.setDragEnabled(true);
864        table.setDropMode(DropMode.ON);
865        table.setDefaultRenderer(String.class,
866                                 new DefaultTableCellRenderer() {
867          final static long serialVersionUID = 42;
868
869          /**
870           * This class controls the background color for table cells.
871           */
872          public Component getTableCellRendererComponent(JTable table,
873                                                         Object value,
874                                                         boolean isSelected,
875                                                         boolean hasFocus,
876                                                         int row,
877                                                         int viewColumn) {
878            if (table == null) {
879              return this;
880            }
881
882            int column = table.convertColumnIndexToModel(viewColumn);
883
884            Color fg = null;
885            Color bg = null;
886
887            String val = data.get(row).get(column);
888            boolean changed = !eql(oldData.get(row).get(column), val);
889
890            JTable.DropLocation dropLocation = table.getDropLocation();
891            if (dropLocation != null
892                && !dropLocation.isInsertRow()
893                && !dropLocation.isInsertColumn()
894                && dropLocation.getRow() == row
895                && table.convertColumnIndexToModel(dropLocation.getColumn()) == column) {
896              fg = Color.blue;
897              bg = Color.pink;
898              isSelected = true;
899            }
900
901            if (isSelected) {
902              super.setForeground(fg == null
903                                  ? table.getSelectionForeground()
904                                  : fg);
905              super.setBackground(bg == null
906                                  ? !changed
907                                  ? table.getSelectionBackground()
908                                  : Color.red
909                                  : bg);
910            } else {
911              Color background = table.getBackground();
912              if(changed) {
913                background = Color.pink;
914              } else if (val == null) {
915                background = Color.gray;
916              }
917              super.setForeground(table.getForeground());
918              super.setBackground(background);
919            }
920            setFont(table.getFont());
921            setBorder(new EmptyBorder(1, 1, 1, 1));
922            setValue(value);
923            return this;
924          }
925        });
926        table.setTransferHandler(new TransferHandler() {
927            final static long serialVersionUID = 42;
928
929            /**
930             * This class handles drag & drop. Strings and files can
931             * be dropped. For files only the file name without
932             * directory information is retained. Dropping multiple
933             * files is not supported.
934             */
935            public boolean canImport(TransferHandler.TransferSupport info) {
936              if (!info.isDrop()) return false; // no clipboard past
937              info.setShowDropLocation(true);
938              if (info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
939                // fetch the drop location
940                JTable.DropLocation dl = (JTable.DropLocation)info.getDropLocation();
941                int row = dl.getRow();
942                int col = table.convertColumnIndexToModel(dl.getColumn());
943                if(col < 0) return false;
944                return true;
945              }
946              if (info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
947                JTable.DropLocation dl = (JTable.DropLocation)info.getDropLocation();
948                int row = dl.getRow();
949                int col = table.convertColumnIndexToModel(dl.getColumn());
950                if(col < 0) return false;
951                return true;
952              }
953              return false;
954            }
955
956            public boolean importData(TransferHandler.TransferSupport info) {
957              if (!canImport(info)) {
958                return false;
959              }
960              JTable.DropLocation dl = (JTable.DropLocation)info.getDropLocation();
961              int row = dl.getRow();
962              int col = table.convertColumnIndexToModel(dl.getColumn());
963              String data;
964              try {
965                if(info.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
966                  List<?> files = (List<?>)info.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
967                  if(files.size() != 1) return false; // can handle only one file
968                  data = ((File)files.get(0)).getName();
969                } else if(info.isDataFlavorSupported(DataFlavor.stringFlavor)) {
970                  data = (String)info.getTransferable().getTransferData(DataFlavor.stringFlavor);
971                } else {
972                  return false;
973                }
974              } catch(UnsupportedFlavorException | IOException e) {
975                return false;
976              }
977              table.getModel().setValueAt(data, row, col);
978              table.changeSelection(row, dl.getColumn(), false, false); // update focus
979              return true;
980            }
981
982            public int getSourceActions(JComponent c) {
983              return COPY;
984            }
985
986            public Transferable createTransferable(JComponent c) {
987              return new StringSelection(data.get(table.getSelectedRow()).get(table.convertColumnIndexToModel(table.getSelectedColumn())));
988            }
989
990            public void exportDone(JComponent c, Transferable t, int i) {
991            }
992          });
993        table.getTableHeader().addMouseListener(new MouseAdapter() {
994            public void mouseClicked(MouseEvent e) {
995              if(e.getButton() == 3) {
996                Point p = e.getPoint();
997                int jj = table.columnAtPoint(p);
998                int j = table.convertColumnIndexToModel(jj);
999                String columnName = Table.this.heading.get(j);
1000                Search search = new Search(Table.this);
1001                DefaultComboBoxModel<String> model = (DefaultComboBoxModel<String>)search.columnField.getModel();
1002                model.setSelectedItem(columnName);
1003                Table.this.dependents.add(search);
1004                Rectangle r = Table.this.getBounds();
1005                search.setLocation(r.x + r.width, r.y);
1006                search.tableField.setText(tableName);
1007                search.pack();
1008                search.setVisible(true);
1009              }
1010            }
1011          });
1012        table.addMouseListener(new MouseAdapter() {
1013            public void mouseClicked(MouseEvent e) {
1014              if(e.getButton() == 3) {
1015                Point p = e.getPoint();
1016                int i = table.rowAtPoint(p);
1017                int jj = table.columnAtPoint(p);
1018                EditWindow cellEditor = makeCellEditWindow(Table.this, i, jj, null);
1019              }
1020            }
1021          });
1022        for (int i = 0; i < heading.size(); i++) {
1023          TableColumn column = table.getColumnModel().getColumn(i);
1024          column.setPreferredWidth(100);
1025        }
1026        ((DefaultTableCellRenderer)table.getTableHeader().getDefaultRenderer())
1027          .setHorizontalAlignment(SwingConstants.LEFT);
1028
1029        JScrollPane scrollPane =
1030          new JScrollPane(table, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
1031                          ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
1032        add(scrollPane);
1033      }
1034    } // class DBTable extends JPanel
1035
1036    /**
1037     * For displaying tables or views
1038     *
1039     * @param tableName the title of the window and table or view to display
1040     */
1041    Table(String tableName) {
1042      this.tableName = tableName;
1043      DatabaseEditor.this.dependents.add(this);
1044      addWindowListener(new WindowAdapter() {
1045          public void windowClosing(WindowEvent e) { dispose(); }
1046          public void windowClosed(WindowEvent e) {
1047            for(JFrame d : dependents) d.dispose();
1048            if(DatabaseEditor.this.dependents.remove(Table.this)) {
1049            } else {
1050              System.out.println("Can't find#3: " + Table.this.getTitle());
1051            }
1052          }
1053        });
1054      setTitle(tableName);
1055      getContentPane().add(new DBTable(null));
1056      JPanel buttons = new JPanel(new GridLayout(2, 4));
1057      buttons.add(new JButton("Delete Selected Rows") {
1058          final static long serialVersionUID = 42;
1059
1060          {
1061            addActionListener(new ActionListener() {
1062                public void actionPerformed(ActionEvent e) {
1063                  TableCellEditor te = table.getCellEditor();
1064                  if(te != null) te.stopCellEditing();
1065                  deleteRows();
1066                }
1067              });
1068          }
1069        });
1070      buttons.add(new JButton("Make NULL") {
1071          final static long serialVersionUID = 42;
1072
1073          {
1074            addActionListener(new ActionListener() {
1075                public void actionPerformed(ActionEvent e) {
1076                  TableCellEditor te = table.getCellEditor();
1077                  if(te != null) te.stopCellEditing();
1078                  makeNull();
1079                }
1080              });
1081          }
1082        });
1083      buttons.add(new JButton("Revert Selected") {
1084          final static long serialVersionUID = 42;
1085
1086          {
1087            addActionListener(new ActionListener() {
1088                public void actionPerformed(ActionEvent e) {
1089                  TableCellEditor te = table.getCellEditor();
1090                  if(te != null) te.stopCellEditing();
1091                  revertSelected();
1092                }
1093              });
1094          }
1095        });
1096      buttons.add(new JButton("Revert Table") {
1097          final static long serialVersionUID = 42;
1098
1099          {
1100            addActionListener(new ActionListener() {
1101                public void actionPerformed(ActionEvent e) {
1102                  TableCellEditor te = table.getCellEditor();
1103                  if(te != null) te.stopCellEditing();
1104                  revertTable();
1105                }
1106              });
1107          }
1108        });
1109      buttons.add(new JButton("Add Row") {
1110          final static long serialVersionUID = 42;
1111
1112          {
1113            addActionListener(new ActionListener() {
1114                public void actionPerformed(ActionEvent e) {
1115                  TableCellEditor te = table.getCellEditor();
1116                  if(te != null) te.stopCellEditing();
1117                  addRow();
1118                }
1119              });
1120          }
1121        });
1122      buttons.add(new JButton("Fill Column") {
1123          final static long serialVersionUID = 42;
1124
1125          {
1126            addActionListener(new ActionListener() {
1127                public void actionPerformed(ActionEvent e) {
1128                  int i = table.getSelectedRow();
1129                  int j = table.convertColumnIndexToModel(table.getSelectedColumn());
1130                  if(i < 0 || j < 0) return;
1131                  if(data.get(i).get(j) == null) {
1132                    if(i == 0) return;
1133                    if(data.get(i - 1).get(j) != null) {
1134                      fillColumn(i, j);
1135                    } else {
1136                      return;
1137                    }
1138                  } else if(i < data.size() - 1 && data.get(i + 1).get(j) == null) {
1139                      fillColumn(i + 1, j);
1140                  } else {
1141                    if(i < data.size() - 2 && data.get(i + 2).get(j) == null) {
1142                      fillColumn(i + 2, j, data.get(i).get(j), data.get(i + 1).get(j));
1143                    }
1144                  }
1145                }
1146              });
1147          }
1148        });
1149      buttons.add(new JButton("Update Selected") {
1150          final static long serialVersionUID = 42;
1151
1152          {
1153            addActionListener(new ActionListener() {
1154                public void actionPerformed(ActionEvent e) {
1155                  TableCellEditor te = table.getCellEditor();
1156                  if(te != null) te.stopCellEditing();
1157                  updateSelected();
1158                }
1159              });
1160          }
1161        });
1162      buttons.add(new JButton("Update Table") {
1163          final static long serialVersionUID = 42;
1164
1165          {
1166            addActionListener(new ActionListener() {
1167                public void actionPerformed(ActionEvent e) {
1168                  TableCellEditor te = table.getCellEditor();
1169                  if(te != null) te.stopCellEditing();
1170                  updateTable();
1171                }
1172              });
1173          }
1174        });
1175      getContentPane().add(buttons, BorderLayout.SOUTH);
1176      setLocationByPlatform(true);
1177    }
1178
1179    /**
1180     * For displaying result sets
1181     *
1182     * @param rs the ResultSet to display
1183     * @param tableName the title of the window
1184     */
1185    Table(ResultSet rs, String tableName) {
1186      this.tableName = tableName;
1187      DatabaseEditor.this.dependents.add(this);
1188      addWindowListener(new WindowAdapter() {
1189          public void windowClosing(WindowEvent e) { dispose(); }
1190          public void windowClosed(WindowEvent e) {
1191            for(JFrame d : dependents) d.dispose();
1192            if(DatabaseEditor.this.dependents.remove(Table.this)) {
1193            } else {
1194              System.out.println("Can't find#4: " + Table.this.getTitle());
1195            }
1196          }
1197        });
1198      setTitle(tableName);
1199      getContentPane().add(new DBTable(rs));
1200      JLabel tableLabel = new JLabel("<html>Table:");
1201      JTextField tableField = new JTextField("temp", 10);
1202      JTextField fileField = new JTextField("data.txt", 10);
1203      JTextArea columnArea = new UndoableTextArea(",");
1204      JTextArea rowArea = new UndoableTextArea("\n");
1205      JTextArea nullArea = new UndoableTextArea("null");
1206      JCheckBox checkBox = new JCheckBox("<html><font size=-2>Escape Strings</font>");
1207      JButton writeButton = new JButton("<html>Write File:") {
1208          final static long serialVersionUID = 42;
1209
1210          {
1211            addActionListener(new ActionListener() {
1212                public void actionPerformed(ActionEvent e) {
1213                  TableCellEditor te = table.getCellEditor();
1214                  if(te != null) te.stopCellEditing();
1215                  outputTable(fileField.getText(),
1216                              columnArea.getText(),
1217                              rowArea.getText(),
1218                              nullArea.getText(),
1219                              checkBox.getModel().isSelected());
1220                }
1221              });
1222          }
1223        };
1224      JButton browseButton = new JButton("<html><font size=-2>Browse</font>") {
1225          final static long serialVersionUID = 42;
1226
1227          {
1228            addActionListener(new ActionListener() {
1229                public void actionPerformed(ActionEvent e) {
1230                  File file = chooseFile("set", new String[][]{});
1231                  if(file == null) return;
1232                  String fileName = file.getPath();
1233                  if(fileName != null) fileField.setText(fileName);
1234                }
1235              });
1236          }
1237        };
1238      JLabel columnLabel = new JLabel("<html>Column Separator:");
1239      JScrollPane columnScrollPane
1240        = new JScrollPane(columnArea,
1241                          ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
1242                          ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
1243      columnScrollPane.setPreferredSize(new Dimension(100, 60));
1244      JLabel rowLabel = new JLabel("<html>Row End:");
1245      JScrollPane rowScrollPane
1246        = new JScrollPane(rowArea,
1247                          ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
1248                          ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
1249      rowScrollPane.setPreferredSize(new Dimension(100, 60));
1250      JLabel nullLabel = new JLabel("<html>null:");
1251      JScrollPane nullScrollPane
1252        = new JScrollPane(nullArea,
1253                          ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
1254                          ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
1255      nullScrollPane.setPreferredSize(new Dimension(80, 60));
1256
1257      Box panel = new Box(BoxLayout.X_AXIS);
1258
1259      Box filePanel = new Box(BoxLayout.Y_AXIS);
1260      panel.add(Box.createHorizontalStrut(5));
1261
1262      filePanel.add(writeButton);
1263      filePanel.add(fileField);
1264      Box browseEscapePanel = new Box(BoxLayout.X_AXIS);
1265      browseEscapePanel.add(browseButton);
1266      browseEscapePanel.add(checkBox);
1267      browseEscapePanel.setAlignmentX(0.0f);
1268      filePanel.add(browseEscapePanel);
1269      panel.add(filePanel);
1270
1271      panel.add(Box.createHorizontalStrut(5));
1272
1273      Box columnPanel = new Box(BoxLayout.Y_AXIS);
1274      columnPanel.add(columnLabel);
1275      columnPanel.add(columnScrollPane);
1276      columnPanel.add(Box.createVerticalGlue());
1277      panel.add(columnPanel);
1278
1279      panel.add(Box.createHorizontalStrut(5));
1280
1281      Box rowPanel = new Box(BoxLayout.Y_AXIS);
1282      rowPanel.add(rowLabel);
1283      rowPanel.add(rowScrollPane);
1284      rowPanel.add(Box.createVerticalGlue());
1285      panel.add(rowPanel);
1286
1287      panel.add(Box.createHorizontalStrut(5));
1288
1289      Box nullPanel = new Box(BoxLayout.Y_AXIS);
1290      nullPanel.add(nullLabel);
1291      nullPanel.add(nullScrollPane);
1292      nullPanel.add(Box.createVerticalGlue());
1293      nullPanel.setMaximumSize(nullPanel.getPreferredSize());
1294      panel.add(nullPanel);
1295
1296      panel.add(Box.createHorizontalGlue());
1297
1298      getContentPane().add(panel, BorderLayout.SOUTH);
1299      setLocationByPlatform(true);
1300    }
1301
1302    /**
1303     * Output a result set to a text file separating rows by a new
1304     * line and cells in a row by the separator String.
1305     *
1306     * @param fileName the name of the file to write
1307     * @param column the cell separator
1308     * @param row the row cell separator
1309     * @param nil the String used for a null entry
1310     * @param escape true if replace ' with ''
1311     */
1312    void outputTable(String fileName, String column, String row, String nil, boolean escape) {
1313      FileOutputStream out = null;
1314      try {
1315        out = new FileOutputStream(fileName);
1316        for(ArrayList<String> a : data) {
1317          for(int i = 0; i < a.size(); i++) {
1318            if(i != 0) out.write(column.getBytes(fileCharset));
1319            String s = a.get(i); // .getBytes(fileCharset);
1320            if(s == null) {
1321              out.write(nil.getBytes(fileCharset));
1322            } else {
1323              out.write((escape ? escapeString(s, '\'') : s).getBytes(fileCharset));
1324            }
1325          }
1326          out.write(row.getBytes(fileCharset));
1327        }
1328      } catch(IOException e) {
1329        e.printStackTrace();
1330      } finally {
1331        close(out);
1332      }
1333    }
1334
1335    /**
1336     * Delete all of the rows that are selected. Then reloads the
1337     * table from the database. Currently only a single cell can be
1338     * selected so this method deletes a single row. Note that this
1339     * method throws away all pending edits that have not been
1340     * committed.
1341     */
1342    void deleteRows() {
1343      Statement stat = null;
1344      try {
1345        stat = conn.createStatement();
1346        for(int i : table.getSelectedRows()) {
1347          int count = stat.executeUpdate("delete from \""
1348                                         + tableName.replace(".", "\".\"")
1349                                         + "\" where (_rowid_ = " + rowid.get(i)
1350                                         + ");");
1351          // count = 1 for both tables and views ???
1352          tableModel.fireTableRowsDeleted(i, i); // is this needed?
1353        }
1354        initTables(null); // update table from database
1355        tableModel.fireTableDataChanged();
1356      } catch(SQLException ex) {
1357        ex.printStackTrace();
1358      } finally {
1359        try{
1360          if(stat != null) stat.close();
1361        } catch(SQLException closeex) {
1362          closeex.printStackTrace();
1363        }
1364      }
1365    }
1366
1367    /**
1368     * This method sets the selected cell to null
1369     */
1370    void makeNull() {
1371      int i = table.getSelectedRow();
1372      int j = table.convertColumnIndexToModel(table.getSelectedColumn());
1373      if(i < 0 || j < 0) return;
1374      ArrayList<String> row = data.get(i);
1375      row.set(j, null);
1376      tableModel.fireTableCellUpdated(i, j);
1377    }
1378
1379    /**
1380     * This method undoes all of the uncommitted edits in the selected
1381     * row. Currently only one cell/row can be selected.
1382     */
1383    void revertSelected() {
1384      int r[] = table.getSelectedRows();
1385      for(int i : r) {
1386        ArrayList<String> row = data.get(i);
1387        ArrayList<String> oldrow = oldData.get(i);
1388        for(int j = 0; j < row.size(); j++) {
1389          String old = oldrow.get(j);
1390          if(!eql(old, row.get(j))) {
1391            row.set(j, old);
1392            tableModel.fireTableCellUpdated(i, j);
1393          }
1394        }
1395      }
1396    }
1397
1398    /**
1399     * This method undoes all of the uncommitted edits in the entire
1400     * table.
1401     */
1402    void revertTable() {
1403      for(int i = 0; i < data.size(); i++) {
1404        ArrayList<String> row = data.get(i);
1405        ArrayList<String> oldrow = oldData.get(i);
1406        for(int j = 0; j < row.size(); j++) {
1407          String old = oldrow.get(j);
1408          if(!eql(old, row.get(j))) {
1409            row.set(j, old);
1410            tableModel.fireTableCellUpdated(i, j);
1411          }
1412        }
1413      }
1414    }
1415
1416    /**
1417     * This method adds a new row to the table. All cells in this new
1418     * row are set to null. If at least one entry is not set non-null
1419     * then this row will not be added to the table. To add an all
1420     * null row first add a row with a cell not null and then edit
1421     * this cell to null (by dragging a null cell value to it).
1422     */
1423    void addRow() {
1424      int oldSize = data.size();
1425      data.add(new ArrayList<String>(Collections.nCopies(heading.size(),
1426                                                      (String)null)));
1427      oldData.add(new ArrayList<String>(Collections.nCopies(heading.size(),
1428                                                         (String)null)));
1429      rowid.add(null);
1430      tableModel.fireTableRowsInserted(oldSize, data.size() - 1);
1431      table.scrollRectToVisible(table.getCellRect(oldSize, -1, true));
1432    }
1433
1434    /**
1435     * This method fills in all cells from cell[i, j] to the end of
1436     * the table stopping at the first nonnull entry
1437     *
1438     * @param i the row number of the first cell to be filled
1439     * @param j the column number of the first cell to be filled
1440     */
1441    void fillColumn(int i, int j) {
1442      String s = data.get(i - 1).get(j);
1443      int size = data.size();
1444      while(i < size && data.get(i).get(j) == null) {
1445        data.get(i).set(j, s);
1446        tableModel.fireTableCellUpdated(i, j);
1447        ++i;
1448      }
1449    }
1450
1451    /**
1452     * This method fills in all cells from cell[i, j] to the end of
1453     * the table stopping at the first nonnull entry. It requires that
1454     * s1 and s2 are Strings of equal length and differ in only one
1455     * character. The differing character is continually incremented
1456     * by the difference between the two differing characters and the
1457     * resulting patched String isinserted in successive cells. This
1458     * makes the successive cells in arithmetic progression. No checks
1459     * are made for generating illegal characters.
1460     *
1461     * @param i the row number of the first cell to be filled
1462     * @param j the column number of the first cell to be filled
1463     * @param s1 the first string in sequence
1464     * @param s2 the second string in sequence
1465     */
1466    void fillColumn(int i, int j, String s1, String s2) {
1467      int length = s1.length();
1468      if(length != s2.length()) return;
1469      int k;
1470      for(k = 0; k < length; ++k) {
1471        if(s1.charAt(k) != s2.charAt(k)) break;
1472      }
1473      if(k == length) {
1474        fillColumn(i + 1, j);
1475        return;
1476      }
1477      char c1 = s1.charAt(k);
1478      char c2 = s2.charAt(k);
1479      int cdiff = c2 - c1;
1480      for(int kk = k + 1; kk < length; ++kk) {
1481        if(s1.charAt(kk) != s2.charAt(kk)) return;
1482      }
1483      // OK now do it
1484      String front = s1.substring(0, k);
1485      String back = s1.substring(k + 1);
1486      char cx = c2;
1487
1488      int size = data.size();
1489      while(i < size && data.get(i).get(j) == null) {
1490        cx = (char)(cx + cdiff);
1491        String s = front + cx + back;
1492        data.get(i).set(j, s);
1493        tableModel.fireTableCellUpdated(i, j);
1494        ++i;
1495      }
1496    }
1497
1498    /**
1499     * This method updates all cells in the selected row.
1500     */
1501    void updateSelected() {
1502      int r[] = table.getSelectedRows();
1503      for(int i : r) {
1504        updateRow(i);
1505        tableModel.fireTableRowsUpdated(i, i);
1506      }
1507    }
1508
1509    /**
1510     * This method updates all cells in the entire table.
1511     */
1512    void updateTable() {
1513      for(int i = 0; i < data.size(); i++) {
1514        updateRow(i);
1515      }
1516      initTables(null);
1517      tableModel.fireTableDataChanged();
1518    }
1519
1520    /**
1521     * New strategy is to use ? instead of putting new values in the
1522     * statement itself. There are row.size() ? marks and the same
1523     * number of putWithCharset(PreparedStatement stat, int i, String
1524     * data) calls. The advantage of the new way is that the character
1525     * encoding can be changed easily (and from the command line)
1526     * although this is currently disables because of the difficulty
1527     * of using blobs. Also avoids sql injection attacks.
1528     *
1529     * This method updates the specified row
1530     *
1531     * @param i the row number to update
1532     */
1533    void updateRow(int i) {
1534      PreparedStatement stat = null;
1535      try {
1536        // rowid = null is insert
1537        ArrayList<String> row = data.get(i);
1538        ArrayList<String> oldRow = oldData.get(i);
1539        String id = rowid.get(i);
1540        if(id == null) { // add row
1541          String colNames = "";
1542          String colValues = "";
1543          for(int j = 0; j < row.size(); j++) {
1544            String old = oldRow.get(j);
1545            if(!eql(old, row.get(j))) {
1546              String h = '"' + heading.get(j) + '"';
1547              colNames += (colNames == "" ? h : "," + h); //////// fails on group as a column name - must use (')s around row name
1548              // "cast(? as text)"
1549              colValues += (colValues == "" ? "?" : ",?");
1550            }
1551          }
1552          if(!colNames.equals("")) {
1553            stat = conn.prepareStatement("insert into \""
1554                                         + tableName.replace(".", "\".\"")
1555                                         + "\"(" + colNames
1556                                         + ") values (" + colValues + ");");
1557            int jj = 1;
1558            for(int j = 0; j < row.size(); j++) {
1559              if(!eql(oldRow.get(j), row.get(j))) {
1560                putWithCharset(stat, jj++, row.get(j));
1561              }
1562            }
1563            int count = stat.executeUpdate();
1564            //count = 1 for normal table and count = 0 for view update
1565            for(int j = 0; j < row.size(); j++) {
1566              oldRow.set(j, row.get(j));
1567            }
1568          }
1569        } else { // update row
1570          for(int j = 0; j < row.size(); j++) {
1571            String newData = row.get(j);
1572            if(!eql(oldRow.get(j), newData)) {
1573              // "cast(? as text)"
1574              stat = conn.prepareStatement("update \""
1575                                           + tableName.replace(".", "\".\"")
1576                                           + "\" set " + heading.get(j)
1577                                           + " = ? where _rowid_ = "
1578                                           + rowid.get(i)
1579                                           + ";");
1580              putWithCharset(stat, 1, row.get(j));
1581              int count = stat.executeUpdate();
1582              //count = 1 for normal table and count = 0 for view update
1583              oldRow.set(j, newData);
1584            }
1585          }
1586        }
1587      } catch(SQLException ex) {
1588        ex.printStackTrace();
1589      } finally {
1590        try {
1591          if (stat != null) stat.close();
1592        } catch(SQLException closeex) {
1593          closeex.printStackTrace();
1594        }
1595      }
1596      tableModel.fireTableRowsUpdated(i, i);
1597    }
1598  } // class Table extends JFrame
1599
1600  /**
1601   * Show the table with the given name
1602   *
1603   * @param tableName the table to display
1604   */
1605  void showTable(String tableName) {
1606    Table frame;
1607    search: {
1608      for(JFrame jf : dependents) {
1609        if(jf.getTitle().equals(tableName)) {
1610          frame = (Table)jf;
1611          frame.setExtendedState(JFrame.NORMAL);
1612          break search;
1613        }
1614      }
1615      frame = new Table(tableName);
1616      frame.pack();
1617      frame.setVisible(true);
1618    }
1619    frame.setVisible(true);
1620    return;
1621  }
1622
1623  File editFile = new File(".", "temp.txt"); // default edit text file
1624
1625  /**
1626   * Make a command window for executing sql statements. The Execute
1627   * Selection button will execute the command in the window or the
1628   * command in the current selection if the current selection is not
1629   * empty.
1630   *
1631   * The Execute Selection button executes the current selection as an
1632   * SQL command. If there is no selection the entire contents of the
1633   * window is executed.
1634   *
1635   * The Select Next Command button selects the sql command following
1636   * the current selection.
1637   *
1638   * The Break Lines in Selection button breaks long line in the
1639   * current selection.
1640   *
1641   * The Print button prints the edit window to a printer.
1642   *
1643   * The Read File button reads the selected file and replaces the
1644   * window contents with the contents of the file. This operation is
1645   * undoable.
1646   *
1647   * The Write File button writes the window contents to the selected
1648   * file.
1649   *
1650   * @param title the title of the window
1651   * @param contents the initial contents of the command window
1652   */
1653  void makeCommandWindow(String title, String contents) {
1654    EditWindow commandWindow = new EditWindow(title, contents, DatabaseEditor.this);
1655    dependents.add(commandWindow); // to database table window
1656
1657    JTextArea textArea = commandWindow.textArea;
1658    JPanel inner = new JPanel(new BorderLayout());
1659    JPanel btns = new JPanel(new GridLayout(4,1));
1660    JLabel status = new JLabel("(status line)");
1661    btns.add(new JButton("Execute Selection") {
1662        final static long serialVersionUID = 42;
1663
1664        {
1665          addActionListener(new ActionListener() {
1666              public void actionPerformed(ActionEvent e) {
1667                Statement stat = null;
1668                try {
1669                  String s = textArea.getSelectedText();
1670                  if(s == null) s = textArea.getText();
1671                  stat = conn.createStatement();
1672                  stat.execute(s);
1673                  int count = stat.getUpdateCount();
1674                  if(count >= 0) {
1675                    status.setText("Update count = " + count);
1676                  } else {
1677                    ResultSet rs = stat.getResultSet();
1678                    if(rs != null) {
1679                      status.setText("Result Set Generated");
1680                      JFrame frame = new Table(rs, "SQL Command Result");
1681                      frame.pack();
1682                      frame.setVisible(true);
1683                    }
1684                  }
1685                } catch(SQLException ex) {
1686                  status.setText(ex.toString());
1687                } finally {
1688                  try {
1689                    if(stat != null) stat.close();
1690                  } catch(SQLException closeex) {
1691                    closeex.printStackTrace();
1692                  }
1693                }
1694                textArea.grabFocus();
1695              }
1696            });
1697        }
1698      });
1699    btns.add(new JButton("Select Next Command") {
1700        final static long serialVersionUID = 42;
1701
1702        {
1703          addActionListener(new ActionListener() {
1704              public void actionPerformed(ActionEvent e) {
1705                String s = textArea.getText();
1706                int end = textArea.getSelectionEnd();
1707                int statementEnd = getEndOfSqlStatement(s, end);
1708                textArea.select(end, statementEnd);
1709                Caret caret = textArea.getCaret();
1710                caret.setVisible(true);
1711                caret.setSelectionVisible(true);
1712              }
1713            });
1714        }
1715      });
1716    btns.add(new JButton("Break Lines in Selection") {
1717        final static long serialVersionUID = 42;
1718
1719        {
1720          addActionListener(new ActionListener() {
1721              public void actionPerformed(ActionEvent e) {
1722                commandWindow.breakLines(70);
1723              }
1724            });
1725        }
1726      });
1727    btns.add(new JButton("Print") {
1728        final static long serialVersionUID = 42;
1729
1730        {
1731          addActionListener(new ActionListener() {
1732              public void actionPerformed(ActionEvent e) {
1733                commandWindow.print("Command Window");
1734              }
1735            });
1736        }
1737      });
1738    btns.add(new JButton("Read File") {
1739        final static long serialVersionUID = 42;
1740
1741        {
1742          addActionListener(new ActionListener() {
1743              public void actionPerformed(ActionEvent e) {
1744                File file = chooseFile("Read",
1745                                       new String[][]{{"*.txt", "txt"}});
1746                if(file != null) {
1747                  editFile = file;
1748                  String d = file.getParent();
1749                  if(d != null) currentDirectory = d;
1750                  String s = readFileIntoString(file);
1751                  if(s != null) {
1752                    textArea.setText(s);
1753                    textArea.setCaretPosition(0);
1754                    textArea.grabFocus();
1755                    commandWindow.setTitle("SQL Command Window - " + file);
1756                  }
1757                }
1758                textArea.setCaretPosition(0);
1759                textArea.grabFocus();
1760              }
1761            });
1762        }
1763      });
1764    btns.add(new JButton("Write File") {
1765        final static long serialVersionUID = 42;
1766
1767        {
1768          addActionListener(new ActionListener() {
1769              public void actionPerformed(ActionEvent e) {
1770                File file = chooseFile("Write",
1771                                       new String[][]{{"*.txt", "txt"}});
1772                if(file != null) {
1773                  editFile = file;
1774                  String d = file.getParent();
1775                  if(d != null) currentDirectory = d;
1776                  OutputStream out = null;
1777                  try {
1778                    out = new FileOutputStream(editFile);
1779                    out.write(textArea.getText().getBytes(fileCharset));
1780                  } catch(IOException ex) {
1781                    ex.printStackTrace();
1782                  } finally {
1783                    close(out);
1784                  }
1785                }
1786                textArea.grabFocus();
1787              }
1788            });
1789        }
1790      });
1791    inner.add(btns, BorderLayout.NORTH);
1792    inner.add(status, BorderLayout.SOUTH);
1793    commandWindow.getContentPane().add(inner, BorderLayout.SOUTH);
1794    commandWindow.setLocationByPlatform(true);
1795    commandWindow.pack();
1796    commandWindow.setVisible(true);
1797  }
1798
1799  /**
1800   * This class provides a search and replace function for table
1801   * entries. It can do global replaces or confine its activities to a
1802   * single field, table, or entry.
1803   *
1804   * Layout:
1805   *
1806   * (((Table |XXX| Field |XXX| _rowid_ |XXX|)))
1807   *
1808   * |   Find   | |XXXXX|   |  Replace   | |XXXXX|
1809   * |Find Funny| |XXXXX|   |Replace/Find| |XXXXX|
1810   *
1811   * Find: select next occurance after current selection
1812   * Find Funny: find the next "funny" character
1813   * Replace: replace current selection with replacement string
1814   * Replace/Find: do a Replace followed by a Find
1815   */
1816  class FindReplace extends JFrame {
1817    final static long serialVersionUID = 42;
1818
1819    JTextArea textArea;
1820    JPanel statusLine = new JPanel(new BorderLayout());
1821    JLabel status = new JLabel("Status area");
1822    JTextArea findField = new UndoableTextArea(4, 15);
1823    JTextArea replaceField = new UndoableTextArea(4, 15);
1824    /**
1825     * Make a find/replace window operating on the given textArea.
1826     *
1827     * @param editWindow the EditWindow to operate on
1828     * @param ignoreCase true if case is ignored in comparisons
1829     */
1830    FindReplace(EditWindow editWindow, boolean ignoreCase) {
1831      this.textArea = editWindow.textArea;
1832      setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // currently no dependents to close
1833      Rectangle r = editWindow.getBounds();
1834      setLocation(r.x + r.width, r.y);
1835      setTitle("Find - Replace");
1836      JPanel args2 = new JPanel();
1837      JPanel btns1 = new JPanel(new GridLayout(2, 1));
1838      JCheckBox checkBox = new JCheckBox("Ignore Case", ignoreCase);
1839      btns1.add(new JButton("Find:") {
1840          final static long serialVersionUID = 42;
1841
1842          {
1843            addActionListener(new ActionListener() {
1844                public void actionPerformed(ActionEvent e) {
1845                  status.setText("");
1846                  String text = textArea.getText();
1847                  String find = findField.getText();
1848                  int end = textArea.getSelectionEnd();
1849                  int location = findIgnoreCase(checkBox.isSelected(), text, find, end);
1850                  if(location < 0) {
1851                    status.setText("not found");
1852                    textArea.select(0, 0);
1853                  } else {
1854                    textArea.select(location, location + find.length());
1855                    editWindow.toFront();
1856                  }
1857                }
1858              });
1859          }
1860        });
1861      btns1.add(new JButton("Find Funny") {
1862          final static long serialVersionUID = 42;
1863
1864          {
1865            addActionListener(new ActionListener() {
1866                public void actionPerformed(ActionEvent e) {
1867                  status.setText("");
1868                  String text = textArea.getText();
1869                  int end = textArea.getSelectionEnd();
1870                  for(int i = end; i < text.length(); i++) {
1871                    char c = text.charAt(i);
1872                    if(c != 0xa && (c < 32 || c > 126)) {
1873                      status.setText("Hex Character = "
1874                                     + Integer.toHexString(c));
1875                      textArea.select(i, i + 1);
1876                      editWindow.toFront();
1877                      return;
1878                    }
1879                  }
1880                  status.setText("not found");
1881                  textArea.select(0, 0);
1882                }
1883              });
1884          }
1885        });
1886      args2.add(btns1);
1887      JScrollPane findScrollPane = new JScrollPane(findField);
1888      args2.add(findScrollPane);
1889      args2.add(new JLabel("      "));
1890      JPanel btns2 = new JPanel(new GridLayout(2, 1));
1891      btns2.add(new JButton("Replace:") {
1892          final static long serialVersionUID = 42;
1893
1894          {
1895            addActionListener(new ActionListener() {
1896                public void actionPerformed(ActionEvent e) {
1897                  status.setText("");
1898                  String replace = replaceField.getText();
1899                  textArea.replaceSelection(replace);
1900                  int end = textArea.getSelectionEnd();
1901                  textArea.select(end - replace.length(), end);
1902                }
1903              });
1904          }
1905        });
1906      args2.add(btns2);
1907      btns2.add(new JButton("Replace/Find:") {
1908          final static long serialVersionUID = 42;
1909
1910          {
1911            addActionListener(new ActionListener() {
1912                public void actionPerformed(ActionEvent e) {
1913                  status.setText("");
1914                  String replace = replaceField.getText();
1915                  textArea.replaceSelection(replace);
1916                  int end = textArea.getSelectionEnd();
1917                  textArea.select(end - replace.length(), end);
1918                  String text = textArea.getText();
1919                  String find = findField.getText();
1920                  int location = findIgnoreCase(checkBox.isSelected(), text, find, end);
1921                  if(location < 0) {
1922                    status.setText("not found");
1923                    textArea.select(0, 0);
1924                  } else {
1925                    textArea.select(location, location + find.length());
1926                    editWindow.toFront();
1927                  }
1928                }
1929              });
1930          }
1931        });
1932      args2.add(btns2);
1933      JScrollPane replaceScrollPane = new JScrollPane(replaceField);
1934      args2.add(replaceScrollPane);
1935      getContentPane().add(args2, BorderLayout.CENTER);
1936      statusLine.add(status, BorderLayout.CENTER);
1937      statusLine.add(checkBox, BorderLayout.EAST);
1938      getContentPane().add(statusLine, BorderLayout.SOUTH);
1939    }
1940  } // class FindReplace extends JFrame
1941
1942  /**
1943   * This class searches many cells in a table column.
1944   * This class provides a search and replace function for table
1945   * entries. It can do global replaces or confine its activities to a
1946   * single field, table, or entry.
1947   *
1948   * Layout:
1949   *
1950   * Table |XXX| Field |XXX|
1951   *
1952   * |   Find   | |XXXXX|   | To Hex | |XXXXX|
1953   * |          | |XXXXX|   |        | |XXXXX|
1954   * |Find Funny| |XXXXX|   |From Hex| |XXXXX|
1955   *
1956   * Find: select next occurance after current selection
1957   * Find Funny: find the next "funny" character
1958   * To Hex: replace contents with hex representation
1959   * Replace/Find: replace hex representation with text
1960   *
1961   * Current state is [table, row, column, textArea position]
1962   * or
1963   * [table selection, textArea position]
1964   *
1965   * Maybe just bring up table, selection, edit window on found instances
1966   */
1967  class Search extends JFrame {
1968    final static long serialVersionUID = 42;
1969
1970    Table table; // the table to search
1971    JTextField tableField = new JTextField(10);
1972    DefaultComboBoxModel<String> model = new DefaultComboBoxModel<String>();
1973    JComboBox<String> columnField = new JComboBox<String>(model);
1974    JCheckBox checkBox = new JCheckBox("Ignore Case", true);
1975    JTextArea findField = new UndoableTextArea(4, 15);
1976    JTextArea replaceField = new UndoableTextArea(4, 15);
1977    JLabel status = new JLabel("Status area");
1978
1979    /**
1980     * Make a find/replace window operating on the given textArea.
1981     *
1982     * @param table the table to operate on
1983     */
1984    Search(Table table) {
1985      this.table = table;
1986      setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // currently no dependents
1987      setTitle("Searching table: " + table.getTitle());
1988      JPanel args1 = new JPanel();
1989      args1.add(new JLabel("Table:"));
1990      args1.add(tableField);
1991      tableField.setEditable(false);
1992      args1.add(new JLabel("    Column:"));
1993      args1.add(columnField);
1994      for(int i = 0; i < table.heading.size(); i++) {
1995        model.addElement(table.heading.get(i));
1996      }
1997      args1.add(checkBox);
1998      getContentPane().add(args1, BorderLayout.NORTH);
1999      JPanel args2 = new JPanel();
2000      JPanel btns1 = new JPanel(new GridLayout(2, 1));
2001      btns1.add(new JButton("Find:") {
2002          final static long serialVersionUID = 42;
2003
2004          {
2005            addActionListener(new ActionListener() {
2006                public void actionPerformed(ActionEvent e) {
2007                  Table table = Search.this.table;
2008                  status.setText("");
2009                  String find = findField.getText();
2010                  String replace = replaceField.getText();
2011                  int column
2012                    = table.heading.indexOf(columnField.getSelectedItem());
2013                  if(column < 0 || column >= table.heading.size()) {
2014                    status.setText("Illegal column specification");
2015                    return; // illegal column
2016                  }
2017                  boolean ignoreCase = checkBox.isSelected();
2018                  for(int row = table.table.getSelectedRow() + 1;
2019                      row < table.data.size();
2020                      ++row) {
2021                    String cell = table.data.get(row).get(column);
2022                    String value = table.data.get(row).get(column);
2023                    if(findIgnoreCase(ignoreCase, value, find, 0) >= 0) {
2024                      int col = table.table.convertColumnIndexToView(column);
2025                      Rectangle r = Search.this.getBounds();
2026                      // bring up an editor window
2027                      EditWindow editWindow = table.makeCellEditWindow(table, row, col, r);
2028                      FindReplace findReplace = new FindReplace(editWindow, ignoreCase);
2029                      editWindow.dependents.add(findReplace);
2030                      findReplace.findField.setText(find);
2031                      findReplace.replaceField.setText(replace);
2032                      findReplace.pack();
2033                      findReplace.setVisible(true);
2034                      return;
2035                    }
2036                  }
2037                  status.setText("not found");
2038                  table.table.clearSelection();
2039                }
2040              });
2041          }
2042        });
2043      btns1.add(new JButton("Find Funny") {
2044          final static long serialVersionUID = 42;
2045
2046          {
2047            addActionListener(new ActionListener() {
2048                public void actionPerformed(ActionEvent e) {
2049                  Table table = Search.this.table;
2050                  status.setText("");
2051                  String find = findField.getText();
2052                  String replace = replaceField.getText();
2053                  int column
2054                    = table.heading.indexOf(columnField.getSelectedItem());
2055                  if(column < 0 || column >= table.heading.size()) {
2056                    status.setText("Illegal column specification");
2057                    return; // illegal column
2058                  }
2059                  for(int row = table.table.getSelectedRow() + 1;
2060                      row < table.data.size();
2061                      ++row) {
2062                    String cell = table.data.get(row).get(column);
2063                    int location = -1; // initialize to not found
2064                    String value = table.data.get(row).get(column);
2065                    if(value != null) {
2066                      for(int i = 0; i < value.length(); i++) {
2067                        char c = value.charAt(i);
2068                        if(c != 0xa && (c < 32 || c > 126)) {
2069                          status.setText("Hex Character = "
2070                                         + Integer.toHexString(c));
2071                          location = 0;
2072                          break;
2073                        }
2074                      }
2075                    }
2076                    if(location >= 0) {
2077                      int col = table.table.convertColumnIndexToView(column);
2078                      // bring up an editor window
2079                      Rectangle r = Search.this.getBounds();
2080                      EditWindow editWindow = table.makeCellEditWindow(table, row, col, r);
2081                      FindReplace findReplace = new FindReplace(editWindow, true);
2082                      editWindow.dependents.add(findReplace);
2083                      findReplace.findField.setText(find);
2084                      findReplace.replaceField.setText(replace);
2085                      findReplace.pack();
2086                      findReplace.setVisible(true);
2087                      return;
2088                    }
2089                  }
2090                  status.setText("not found");
2091                  table.table.clearSelection();
2092                }
2093              });
2094          }
2095        });
2096      args2.add(btns1);
2097      JScrollPane findScrollPane = new JScrollPane(findField);
2098      args2.add(findScrollPane);
2099      args2.add(new JLabel("      "));
2100      JPanel btns2 = new JPanel(new GridLayout(2, 1));
2101      btns2.add(new JButton("To Hex:") {
2102          final static long serialVersionUID = 42;
2103
2104          {
2105            addActionListener(new ActionListener() {
2106                public void actionPerformed(ActionEvent e) {
2107                  String replace = replaceField.getText();
2108                  StringBuilder hex = new StringBuilder();
2109                  for(int i = 0; i < replace.length(); i++) {
2110                    if(hex.length() != 0) hex.append('|');
2111                    hex.append(Integer.toHexString(replace.charAt(i)));
2112                  }
2113                  replaceField.setText(hex.toString());
2114                }
2115              });
2116          }
2117        });
2118      args2.add(btns2);
2119      btns2.add(new JButton("From Hex:") {
2120          final static long serialVersionUID = 42;
2121
2122          {
2123            addActionListener(new ActionListener() {
2124                public void actionPerformed(ActionEvent e) {
2125                  String replace = replaceField.getText();
2126                  StringBuilder hex = new StringBuilder();
2127                  int charValue = 0;
2128                  boolean haveValue = false;
2129                  for(int i = 0; i < replace.length(); i++) {
2130                    char c = replace.charAt(i);
2131                    if(c >= '0' && c <= '9') {
2132                      haveValue = true;
2133                      charValue = 16*charValue + c - '0';
2134                    } else if(c >= 'A' && c <= 'F') {
2135                      haveValue = true;
2136                      charValue = 16*charValue + c - 'A' + 10;
2137                    } else if(c >= 'a' && c <= 'f') {
2138                      haveValue = true;
2139                      charValue = 16*charValue + c - 'a' + 10;
2140                    } else {
2141                      if(haveValue) {
2142                        hex.append((char)charValue);
2143                        charValue = 0;
2144                        haveValue = false;
2145                      }
2146                    }
2147                  }
2148                  if(haveValue) {
2149                    hex.append((char)charValue);
2150                    haveValue = false;
2151                  }
2152                  replaceField.setText(hex.toString());
2153                }
2154              });
2155          }
2156        });
2157      args2.add(btns2);
2158      JScrollPane replaceScrollPane = new JScrollPane(replaceField);
2159      args2.add(replaceScrollPane);
2160      getContentPane().add(args2, BorderLayout.CENTER);
2161      getContentPane().add(status, BorderLayout.SOUTH);
2162    }
2163  } // class Search extends JFrame
2164
2165  /**
2166   * This class is identical to a JTextArea but also has undo / redo
2167   * capability.
2168   *
2169   * cntrl-Z is undo
2170   * cntrl-Y is redo
2171   */
2172  class UndoableTextArea extends JTextArea {
2173    final static long serialVersionUID = 42;
2174
2175    UndoableTextArea() {
2176      super();
2177    }
2178
2179    UndoableTextArea(Document doc) {
2180      super(doc);
2181    }
2182
2183    UndoableTextArea(Document doc, String text, int rows, int columns) {
2184      super(doc, text, rows, columns);
2185    }
2186
2187    UndoableTextArea(int rows, int columns) {
2188      super(rows, columns);
2189    }
2190
2191    UndoableTextArea(String text) {
2192      super(text);
2193    }
2194
2195    UndoableTextArea(String text, int rows, int columns) {
2196      super(text, rows, columns);
2197    }
2198
2199    {
2200      UndoManager undoManager = new UndoManager();
2201      undoManager.setLimit(-1); // unlimited undos
2202      Document doc = getDocument();
2203      doc.addUndoableEditListener(new UndoableEditListener() {
2204          //@Override
2205          public void undoableEditHappened(UndoableEditEvent e) {
2206            undoManager.addEdit(e.getEdit());
2207          }
2208        });
2209
2210      InputMap im = getInputMap(JComponent.WHEN_FOCUSED);
2211      ActionMap am = getActionMap();
2212
2213      Toolkit toolkit = Toolkit.getDefaultToolkit();
2214
2215      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_Z,
2216                                    toolkit.getMenuShortcutKeyMask()),
2217             "Undo");
2218      im.put(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
2219                                    toolkit.getMenuShortcutKeyMask()),
2220             "Redo");
2221
2222      am.put("Undo", new AbstractAction() {
2223          final static long serialVersionUID = 42;
2224          //@Override
2225          public void actionPerformed(ActionEvent e) {
2226            try {
2227              if (undoManager.canUndo()) {
2228                undoManager.undo();
2229              }
2230            } catch (CannotUndoException exp) {
2231              exp.printStackTrace();
2232            }
2233          }
2234        });
2235      am.put("Redo", new AbstractAction() {
2236          final static long serialVersionUID = 42;
2237         //@Override
2238          public void actionPerformed(ActionEvent e) {
2239            try {
2240              if (undoManager.canRedo()) {
2241                undoManager.redo();
2242              }
2243            } catch (CannotUndoException exp) {
2244              exp.printStackTrace();
2245            }
2246          }
2247        });
2248    }
2249  } // class UndoableTextArea extends JTextArea
2250
2251  // Here are classes and methods used to dump the database. Note that
2252  // the method println is not used as this method outputs a
2253  // platform-dependent end-of-line character.
2254
2255  /**
2256   * Abstract class to operate on rows of a table.
2257   */
2258  abstract static class RowProc {
2259    /**
2260     * Method called on each row of a table resulting from a query.
2261     *
2262     * @param row the row of the table resulting from a query
2263     */
2264    abstract void doRow(ArrayList<String> row);
2265  } // abstract class RowProc
2266
2267  /**
2268   * Queries a database and calls a method on each row of the result set.
2269   *
2270   * @param query the query (select statement) to execute
2271   * @param rowProc a class encapsulating the method to run on each row
2272   */
2273  void queryTable(String query, RowProc rowProc) {
2274    ArrayList<String> row = new ArrayList<String>();
2275    Statement stat = null;
2276    try {
2277      stat = conn.createStatement();
2278      stat.execute(query);
2279      int count = stat.getUpdateCount();
2280      if(count >= 0) {
2281        System.err.println("No table result");
2282        System.err.println(query);
2283        return;
2284      }
2285      ResultSet rs = stat.getResultSet();
2286      ResultSetMetaData md = rs.getMetaData();
2287      int tablecolumns = md.getColumnCount();
2288      ArrayList<Integer> columnIndex = new ArrayList<Integer>();
2289      while(rs.next()) {
2290        ArrayList<String> data = new ArrayList<String>();
2291        for(int i = 1; i <= tablecolumns; i++) {
2292          data.add(getWithCharset(rs, i));
2293        }
2294        rowProc.doRow(data);
2295      }
2296    } catch(SQLException e) {
2297      System.err.println("SQL problem in queryTable function:");
2298      System.err.println(query);
2299      e.printStackTrace();
2300      return;
2301      //throw e;
2302    } finally {
2303      try {
2304        if(stat != null) stat.close();
2305      } catch(SQLException e) {
2306        e.printStackTrace();
2307      }
2308    }
2309  }
2310
2311  /**
2312   * Run query and output result followed by a ';' for each
2313   * row. Assume only one column in the result.
2314   *
2315   * @param out the OutputStream to be used for output
2316   * @param query the query string
2317   */
2318  void runTableDumpQuery(OutputStream out, String query) {
2319    queryTable(query, new RowProc() {
2320        void doRow(ArrayList<String> row) {
2321          try {
2322          if(row.size() != 1) System.err.println("### error number of columns in runTableDumpQuery");
2323          String s = row.get(0);
2324          if (s == null) out.write("null".getBytes(fileCharset));
2325          else out.write(s.getBytes(fileCharset));
2326          if(s.indexOf("--") >= 0) out.write("\n;\n".getBytes(fileCharset));
2327          else out.write(";\n".getBytes(fileCharset));
2328          } catch(IOException e) {
2329            e.printStackTrace();
2330          }
2331        }
2332      });
2333  }
2334
2335  /**
2336   * Dump table etc. in the main database.
2337   *
2338   * @param out the OutputStream to be used for output
2339   * @param query the query string
2340   */
2341  void runSchemaDumpQuery(OutputStream out, String query) {
2342    queryTable(query, new RowProc() {
2343        void doRow(ArrayList<String> row) {
2344          try {
2345            String table = row.get(0);
2346            String type = row.get(1);
2347            String sql = row.get(2);
2348            if("sqlite_sequence".equals(table)) {
2349              out.write("DELETE FROM sqlite_sequence;\n".getBytes(fileCharset));
2350            } else if(table.length() == 12 && table.substring(0,11).equals("sqlite_stat")) {
2351              out.write("ANALYZE sqlite_master;\n".getBytes(fileCharset));
2352            } else if(table.length() >= 7 && table.substring(0,7).equals("sqlite_")) {
2353              return;
2354            } else if(table.length() >= 20 && table.substring(0,20).equals("CREATE VIRTUAL TABLE")) {
2355              // not handled
2356              return;
2357            } else {
2358              out.write((sql + ";\n").getBytes(fileCharset));
2359            }
2360            if(type.equals("table")) {
2361              StringBuilder names = new StringBuilder();
2362              queryTable("PRAGMA table_info(" + escapeString(table, '"') + ");",
2363                         new RowProc() {
2364                           void doRow(ArrayList<String> row) {
2365                             if(names.length() != 0) names.append("||','||");
2366                             names.append("quote(");
2367                             names.append(escapeString(row.get(1), '"'));
2368                             names.append(")");
2369                           }
2370                         });
2371              String query = "SELECT 'INSERT INTO ' || "
2372                + escapeString(table, '"')
2373                + " || ' VALUES(' || "
2374                + names
2375                + "|| ')' FROM  "
2376                + escapeString(table, '"');
2377              runTableDumpQuery(out, query);
2378            }
2379          } catch(IOException e) {
2380            e.printStackTrace();
2381          }
2382          return;
2383        }
2384      });
2385  }
2386
2387  File backupFile = new File(".", "database.txt"); // default backup and restore text file
2388
2389  /**
2390   * Dump the main database
2391   *
2392   * @param out the OutputStream to be used for output
2393   */
2394  void dumpDatabase(OutputStream out) {
2395    try{
2396      out.write("PRAGMA foreign_keys=OFF;\n".getBytes(fileCharset));
2397      out.write("BEGIN TRANSACTION;\n".getBytes(fileCharset));
2398      // "SAVEPOINT dump; PRAGMA writable_schema=ON"
2399      runSchemaDumpQuery(out,
2400                         "SELECT name, type, sql FROM sqlite_master " +
2401                         "WHERE sql NOT NULL AND type=='table' AND name!='sqlite_sequence'");
2402      runSchemaDumpQuery(out,
2403                         "SELECT name, type, sql FROM sqlite_master " +
2404                         "WHERE name=='sqlite_sequence'");
2405      runTableDumpQuery(out,
2406                        "SELECT sql FROM sqlite_master " +
2407                        "WHERE sql NOT NULL AND type IN ('index','trigger','view')");
2408      out.write("COMMIT;\n".getBytes(fileCharset));
2409    } catch(IOException e) {
2410      e.printStackTrace();
2411    }
2412  }
2413
2414  static boolean IdChar(char c) {
2415    if('A' <= c && c <= 'Z') return true;
2416    if('a' <= c && c <= 'z') return true;
2417    if('0' <= c && c <= '9') return true;
2418    if(c == '_') return true;
2419    return false;
2420  }
2421
2422  /**
2423   * Returns the line number of a character position in a string
2424   *
2425   * @param s the string to search
2426   * @param charPos the position in the string
2427   * @return the line number (one-based) of the character position
2428   */
2429  static int lineNumber(String s, int charPos) {
2430    int count = 0;
2431    for(int i = 0; i < charPos; i++) {
2432      if(s.charAt(i) == '\n') count++;
2433    }
2434    return count;
2435  }
2436
2437  /**
2438   * Check to see if the String query is at position in String s. This comparison ignores case.
2439   *
2440   * @param s the base String to check
2441   * @param position character position in s to check
2442   * @param query the string to match
2443   * @return true if String query is in s at position
2444   */
2445  static boolean stringCompare(String s, int position, String query) {
2446    if(s == null) return false; // may not be needed
2447    int sLength = s.length();
2448    int qLength = query.length();
2449    if(position + qLength > sLength) return false;
2450    return s.substring(position, position + qLength).compareToIgnoreCase(query) == 0;
2451  }
2452
2453  // State machine for partially parsing SQL statements.
2454  static final int trans[][] = new int[][]{
2455                /* Token:                                                */
2456    /* State:    **  SEMI  WS  OTHER  EXPLAIN  CREATE  TEMP  TRIGGER  END */
2457    /* 0 INVALID: */ { 1,  0,     2,       3,      4,    2,       2,   2, },
2458    /* 1   START: */ { 1,  1,     2,       3,      4,    2,       2,   2, },
2459    /* 2  NORMAL: */ { 1,  2,     2,       2,      2,    2,       2,   2, },
2460    /* 3 EXPLAIN: */ { 1,  3,     3,       2,      4,    2,       2,   2, },
2461    /* 4  CREATE: */ { 1,  4,     2,       2,      2,    4,       5,   2, },
2462    /* 5 TRIGGER: */ { 6,  5,     5,       5,      5,    5,       5,   5, },
2463    /* 6    SEMI: */ { 6,  6,     5,       5,      5,    5,       5,   7, },
2464    /* 7     END: */ { 1,  7,     5,       5,      5,    5,       5,   5, },
2465  };
2466
2467  /**
2468   * Find the end of an SQL staement starting at character position
2469   * start. Only works for valid SQL statements. Note that the
2470   * discovered SQL statement may not be valid for illegal SQL
2471   * statements. The complexity is because of the syntax of CREATE
2472   * TRIGGER statements. This code was taken from the sqlite database
2473   * C code.
2474   *
2475   * This implementation uses a state machine with 8 states:
2476   *
2477   *   (0) INVALID We have not yet seen a non-whitespace character.
2478   *
2479   *   (1) START At the beginning or end of an SQL statement.  This
2480   *   routine returns if it ends in the START state or reaches the
2481   *   end of the string.
2482   *
2483   *   (2) NORMAL We are in the middle of statement which ends with a
2484   *   single semicolon.
2485   *
2486   *   (3) EXPLAIN The keyword EXPLAIN has been seen at the beginning
2487   *   of a statement.
2488   *
2489   *   (4) CREATE The keyword CREATE has been seen at the beginning of
2490   *   a statement, possibly preceded by EXPLAIN and/or followed by
2491   *   TEMP or TEMPORARY
2492   *
2493   *   (5) TRIGGER We are in the middle of a trigger definition that
2494   *   must be ended by a semicolon, the keyword END, and another
2495   *   semicolon.
2496   *
2497   *   (6) SEMI We've seen the first semicolon in the ";END;" that
2498   *   occurs at the end of a trigger definition.
2499   *
2500   *   (7) END We've seen the ";END" of the ";END;" that occurs at the
2501   *   end of a trigger definition.
2502   *
2503   * @param s the String of SQL statements
2504   * @param start the character position of the beginning of the SQL statement
2505   * @return the character position one beyond the end of the discovered statement
2506   */
2507  int getEndOfSqlStatement(String s, int start) {
2508    final int tkSEMI = 0;    //  A semicolon.
2509    final int tkWS = 1;      //  Whitespace.
2510    final int tkOTHER = 2;   //  Any other SQL token.
2511    final int tkEXPLAIN = 3; //  The "explain" keyword.
2512    final int tkCREATE = 4;  //  The "create" keyword.
2513    final int tkTEMP = 5;    //  The "temp" or "temporary" keyword.
2514    final int tkTRIGGER = 6; //  The "trigger" keyword.
2515    final int tkEND = 7;     //  The "end" keyword.
2516
2517    int sLength = s.length();
2518    int state = 0;   /* Current state, using numbers defined in header comment */
2519    int token;       /* Value of the next token */
2520    int p = start;
2521    while(state != 1 && p < sLength) {
2522      loop: switch( s.charAt(p) ) {
2523      case ';': {  /* A semicolon */
2524        token = tkSEMI;
2525        break;
2526      }
2527      case ' ':
2528      case '\r':
2529      case '\t':
2530      case '\n':
2531      case '\f': {  /* White space is ignored */
2532        token = tkWS;
2533        break;
2534      }
2535      case '/': {   /* C-style comments */
2536        if( p + 1 >= sLength || s.charAt( p + 1 ) !='*' ) {
2537          token = tkOTHER;
2538          break loop;
2539        }
2540        p += 2;
2541        while( p < sLength && !stringCompare(s, p, "*/") ) p++;
2542        if(p < sLength) p++;
2543        token = tkWS;
2544        break loop;
2545      }
2546      case '-': {   /* SQL-style comments from "--" to end of line */
2547        if( p + 1 < sLength && s.charAt(p + 1)!='-' ) {
2548          token = tkOTHER;
2549          break loop;
2550        }
2551        while( p < sLength && s.charAt(p) != '\n' ) { p++; }
2552        token = tkWS;
2553        break loop;
2554      }
2555      case '[': {   /* Microsoft-style identifiers in [...] */
2556        p++;
2557        while( p < sLength && s.charAt(p) != ']' ) { p++; }
2558        token = tkOTHER;
2559        break loop;
2560      }
2561      case '`':     /* Grave-accent quoted symbols used by MySQL */
2562      case '"':     /* single- and double-quoted strings */
2563      case '\'': {
2564        char c = s.charAt(p);
2565        p++;
2566        while( p < sLength && s.charAt(p)!=c ) { p++; }
2567        token = tkOTHER;
2568        break loop;
2569      }
2570      default: {
2571        if( IdChar(s.charAt(p)) ) {
2572          /* Keywords and unquoted identifiers */
2573          int nId;
2574          for(nId=1;
2575              p + nId < sLength && IdChar(s.charAt(p + nId));
2576              nId++) {}
2577          inner: switch( s.charAt(p) ) {
2578          case 'c': case 'C': {
2579            if( nId==6 && stringCompare(s, p, "create") ) {
2580              token = tkCREATE;
2581            }else{
2582              token = tkOTHER;
2583            }
2584            break inner;
2585          }
2586          case 't': case 'T': {
2587            if( nId==7 && stringCompare(s, p, "trigger") ) {
2588              token = tkTRIGGER;
2589            }else if( nId==4 && stringCompare(s, p, "temp") ) {
2590              token = tkTEMP;
2591            }else if( nId==9 && stringCompare(s, p, "temporary") ) {
2592              token = tkTEMP;
2593            }else{
2594              token = tkOTHER;
2595            }
2596            break inner;
2597          }
2598          case 'e':  case 'E': {
2599            if( nId==3 && stringCompare(s, p, "end") ) {
2600              token = tkEND;
2601            }else
2602              if( nId==7 && stringCompare(s, p, "explain") ) {
2603                token = tkEXPLAIN;
2604              }else {
2605                token = tkOTHER;
2606              }
2607            break inner;
2608          }
2609          default: {
2610            token = tkOTHER;
2611            break inner;
2612          }
2613          }
2614          p += nId-1;
2615        }else{
2616          /* Operators and special symbols */
2617          token = tkOTHER;
2618        }
2619        break loop;
2620      }
2621      }
2622      state = trans[state][token];
2623      p++;
2624    }
2625    return p;
2626  }
2627
2628  static String currentDirectory = ".";
2629
2630  /**
2631   * Bring up a file browser to choose a file.
2632   *
2633   * Second argument is a String[][] where each array is {desc, ext, ext, ext}.
2634   * this matches the arguments to Class FileNameExtensionFilter
2635   *
2636   * @param approve the label in the approve button
2637   * @param filters a description of the filters to be used
2638   * @return the name of the chosen file or null if no selection
2639   */
2640  static File chooseFile(String approve, String[]...filters) {
2641    File file;
2642    JFileChooser fc = new JFileChooser(currentDirectory);
2643    fc.setFileHidingEnabled(false);
2644    fc.setApproveButtonText(approve);
2645    for(String[] filter : filters) {
2646      String[] extensions = Arrays.copyOfRange(filter, 1, filter.length);
2647      fc.addChoosableFileFilter(new FileNameExtensionFilter(filter[0],
2648                                                            extensions));
2649    }
2650    int status = fc.showDialog(null, approve);
2651    if (status == JFileChooser.APPROVE_OPTION) {
2652      file = fc.getSelectedFile();
2653    } else {
2654      file = null;
2655    }
2656    if(file != null) {
2657      String d = file.getParent();
2658      if(d != null) currentDirectory = d;
2659    }
2660    return file;
2661  }
2662
2663  /**
2664   * This class is a pure data structure.
2665   */
2666  static class DatabaseTableEntry implements Comparable<DatabaseTableEntry> {
2667    String dbName;
2668    String tableName;
2669    String type;
2670    boolean ambiguous = false;
2671
2672    /**
2673     * Create a DatabaseTableEntry.
2674     *
2675     * @param dbName the database name
2676     * @param tableName the name of the table in the database
2677     * @param type table or view or index or trigger etc.
2678     */
2679    DatabaseTableEntry(String dbName, String tableName, String type) {
2680      this.dbName = dbName;
2681      this.tableName = tableName;
2682      this.type = type;
2683    }
2684
2685    public int compareTo(DatabaseTableEntry other) {
2686      int c = tableName.compareTo(other.tableName);
2687      if(c != 0) return c;
2688      ambiguous = other.ambiguous = true;
2689      c = dbName.compareTo(other.dbName);
2690      return c;
2691    }
2692
2693    String fullName() {
2694      return dbName + '.' + tableName;
2695    }
2696
2697    public String toString() {
2698      if(!ambiguous) return tableName;
2699      return dbName + '.' + tableName;
2700    }
2701  } // static class DatabaseTableEntry implements Comparable<DatabaseTableEntry>
2702
2703  ArrayList<DatabaseTableEntry> tables;
2704  JList<Object> list;
2705  boolean hidden;
2706
2707  /**
2708   * Bring up a database editor
2709   *
2710   * @param db the name of the database to display
2711   */
2712  static void makeTableList(String db) {
2713    if(db == null) {
2714      File file = chooseFile("Open",
2715                             new String[][]{{"*.db *.sqlite", "db", "sqlite"}});
2716      if(file == null) return;
2717      db = file.getPath();
2718    }
2719    if(db == null) return;
2720    try {
2721      Connection conn = DriverManager.getConnection("jdbc:sqlite:" + db);
2722      DatabaseEditor frame = new DatabaseEditor(conn, db, false);
2723      frame.pack();
2724      frame.setVisible(true);
2725    } catch(SQLException ex) {
2726      ex.printStackTrace();
2727    }
2728  }
2729
2730  /**
2731   * Create a database editor
2732   *
2733   * @param conn the database connection
2734   * @param db the name of the database
2735   * @param hidden true if showing hidden tables (always called with false)
2736   */
2737  DatabaseEditor(Connection conn, String db, boolean hidden) {
2738    this.hidden = hidden;
2739    this.conn = conn;
2740    addWindowListener(new WindowAdapter() {
2741        public void windowClosing(WindowEvent e) {DatabaseEditor.this.dispose(); }
2742        public void windowClosed(WindowEvent e) {
2743          for(JFrame d : dependents) d.dispose();
2744          try {
2745            conn.close();
2746          } catch(SQLException ex) {
2747            System.err.println("Exception on closing top window");
2748            ex.printStackTrace(); ///////////// ???????????
2749          }
2750        }
2751      });
2752    setLocationByPlatform(true);
2753    setTitle(db);
2754    initList(hidden);
2755    list = new JList<Object>(tables.toArray()) {
2756        final static long serialVersionUID = 42;
2757
2758        public String getToolTipText(MouseEvent e) {
2759          int index = locationToIndex(e.getPoint());
2760          if (index > -1) {
2761            DatabaseTableEntry item
2762              = (DatabaseTableEntry)getModel().getElementAt(index);
2763            return item.dbName + '.' + item.tableName;
2764          }
2765          return null;
2766        }
2767      };
2768    list.setCellRenderer(new DefaultListCellRenderer() {
2769        final static long serialVersionUID = 42;
2770
2771        public Component getListCellRendererComponent(JList<?> list,
2772                                                      Object value,
2773                                                      int index,
2774                                                      boolean isSelected,
2775                                                      boolean cellHasFocus) {
2776          DatabaseTableEntry entry = (DatabaseTableEntry)value;
2777          JLabel label = new JLabel(entry.toString());
2778          label.setOpaque(true);
2779          // 0xffffc0 is light yellow
2780          if(entry.type.equals("view")) label.setBackground(new Color(0xffffc0));
2781          return label;
2782        }
2783      });
2784    JScrollPane scrollPane = new JScrollPane(list);
2785    scrollPane.setPreferredSize(new Dimension(200, 600));
2786    getContentPane().add(scrollPane);
2787    list.addMouseListener(new MouseAdapter() {
2788        public void mouseClicked(MouseEvent e) {
2789          if(e.getButton() == 1) {
2790            int i = list.locationToIndex(e.getPoint());
2791            if(i >= 0) {
2792              showTable(tables.get(i).fullName());
2793            }
2794          } else if(e.getButton() == 3) {
2795            int i = list.locationToIndex(e.getPoint());
2796            if(i >= 0) {
2797              String tableName = tables.get(i).toString();
2798              Table table = new Table(tableName);
2799              table.pack();
2800              table.setVisible(true);
2801              Search search = new Search(table);
2802              table.dependents.add(search);
2803              Rectangle r = table.getBounds();
2804              search.setLocation(r.x + r.width, r.y);
2805              search.tableField.setText(tableName);
2806              search.pack();
2807              search.setVisible(true);
2808            }
2809          }
2810        }
2811      });
2812
2813    JPanel buttons = new JPanel(new GridLayout(6, 1));
2814
2815    buttons.add(new JButton("Refresh") {
2816        final static long serialVersionUID = 42;
2817
2818        {
2819          addActionListener(new ActionListener() {
2820              public void actionPerformed(ActionEvent e) {
2821                initList(DatabaseEditor.this.hidden);
2822                list.setListData(tables.toArray());
2823              }
2824            });
2825        }
2826      });
2827    buttons.add(new JButton("Show Hidden Tables") {
2828        final static long serialVersionUID = 42;
2829
2830        {
2831          addActionListener(new ActionListener() {
2832              public void actionPerformed(ActionEvent e) {
2833                DatabaseEditor.this.hidden = !DatabaseEditor.this.hidden;
2834                setText(DatabaseEditor.this.hidden
2835                        ? "Hide Hidden Tables"
2836                        : "Show Hidden Tables");
2837                initList(DatabaseEditor.this.hidden);
2838                list.setListData(tables.toArray());
2839              }
2840            });
2841        }
2842      });
2843    buttons.add(new JButton("Open Command Window") {
2844        final static long serialVersionUID = 42;
2845
2846        {
2847          addActionListener(new ActionListener() {
2848              public void actionPerformed(ActionEvent e) {
2849                makeCommandWindow("SQL Command Window", "");
2850              }
2851            });
2852        }
2853      });
2854    buttons.add(new JButton("Open Database") {
2855        final static long serialVersionUID = 42;
2856
2857        {
2858          addActionListener(new ActionListener() {
2859              public void actionPerformed(ActionEvent e) {
2860                makeTableList(null);
2861              }
2862            });
2863        }
2864      });
2865    buttons.add(new JButton("Read Database") {
2866        final static long serialVersionUID = 42;
2867
2868        {
2869          addActionListener(new ActionListener() {
2870              public void actionPerformed(ActionEvent e) {
2871                File file = chooseFile("Read", new String[][]{});
2872                if(file != null) {
2873                  backupFile = file;
2874                  String d = file.getParent();
2875                  if(d != null) currentDirectory = d;
2876                  String fileString = readFileIntoString(backupFile);
2877                  if(fileString != null) {
2878                    int start = 0;
2879                    int end = 0;
2880                    Statement stat = null;
2881                    try {
2882                      do {
2883                        end = getEndOfSqlStatement(fileString, start);
2884                        stat = conn.createStatement();
2885                        stat.execute(fileString.substring(start, end));
2886                        int count = stat.getUpdateCount();
2887                        if(count >= 0) {
2888                          //status.setText("Update count = " + count);
2889                        } else {
2890                          ResultSet rs = stat.getResultSet();
2891                          if(rs != null) {
2892                            //status.setText("Result Set Generated");
2893                            JFrame frame = new Table(rs, "SQL Command Result");
2894                            frame.pack();
2895                            frame.setVisible(true);
2896                          }
2897                        }
2898                        stat.close();
2899                        start = end;
2900                      } while(start + 3 < fileString.length());
2901                      System.out.println("Done!");
2902                    } catch(SQLException ex) {
2903                      int startLine = lineNumber(fileString, start);
2904                      int endLine = lineNumber(fileString, end);
2905                      System.err.println("error at lines " + startLine + " to " + endLine + ".");
2906                      System.err.println(fileString.substring(start, end));
2907                      System.err.println(ex.getMessage());
2908                    } finally {
2909                      try{
2910                        if(stat != null) stat.close();
2911                      } catch(SQLException closeex) {
2912                        closeex.printStackTrace();
2913                      }
2914                    }
2915                  }
2916                }
2917              }
2918            });
2919        }
2920      });
2921    buttons.add(new JButton("Write Database") {
2922        final static long serialVersionUID = 42;
2923
2924        {
2925          addActionListener(new ActionListener() {
2926              public void actionPerformed(ActionEvent e) {
2927                File file = chooseFile("Write", new String[][]{});
2928                if(file != null) {
2929                  backupFile = file;
2930                  String d = file.getParent();
2931                  if(d != null) currentDirectory = d;
2932                  OutputStream out = null;
2933                  try {
2934                    out = new FileOutputStream(backupFile);
2935                    dumpDatabase(out);
2936                  } catch(IOException ex) {
2937                    ex.printStackTrace();
2938                  } finally {
2939                    close(out);
2940                  }
2941                }
2942              }
2943            });
2944        }
2945      });
2946    getContentPane().add(buttons, BorderLayout.SOUTH);
2947  }
2948
2949  /**
2950   * Using the sqlite_master and sqlite_temp_master tables construct a
2951   * list of all tables in the attached databases.
2952   *
2953   * Currently assume table and database names do not contain "." or
2954   * use "." in db.name
2955   *
2956   * Note that the sqlite_master and sqlite_temp_master tables are not
2957   * in the list of tables so they have to be added separately.
2958   *
2959   * @param hidden true if listing hidden tables
2960   */
2961  void initList(boolean hidden) {
2962    tables = new ArrayList<DatabaseTableEntry>();
2963    try {
2964      Statement dbstat = conn.createStatement();
2965      ResultSet dbrs = dbstat.executeQuery("pragma database_list;");
2966      ResultSetMetaData dbmd = dbrs.getMetaData();
2967      while(dbrs.next()) {
2968        String database = dbrs.getString(2);
2969        String master = database.equals("temp") ? "sqlite_temp_master"
2970          : "sqlite_master";
2971        if(hidden) {
2972          tables.add(new DatabaseTableEntry(database, master, "table"));
2973        }
2974        Statement stat = conn.createStatement();
2975        ResultSet rs = stat.executeQuery("select * from \""
2976                                         + database + "\".\"" + master
2977                                         + "\";");
2978        ResultSetMetaData md = rs.getMetaData();
2979        while (rs.next()) {
2980          String type = rs.getString(1);
2981          if(type.equals("table") || type.equals("view")) {
2982            String tableName = rs.getString(2);
2983            if(hidden || tableName.indexOf("sqlite_") != 0) {
2984              tables.add(new DatabaseTableEntry(database, tableName, type));
2985            }
2986          }
2987        }
2988        stat.close();
2989      }
2990      dbstat.close();
2991      Collections.sort(tables);
2992    } catch(SQLException ex) {
2993      System.err.println("Editor initialization failed for " + ex);
2994      ex.printStackTrace();
2995    }
2996  }
2997
2998  /**
2999   * View or edit a sqlite database. Note that there are two character
3000   * sets - one for the database and one for the file system. They
3001   * both default to UTF-8.
3002   *
3003   * Usage: java DatabaseEditor &lt;dbname&gt; [&lt;dbCharset&gt; [&lt;File System charset&gt;]]
3004   *
3005   * @param args The command line arguments.
3006   */
3007  public static void main(String[] args) {
3008    try {
3009      Class.forName("org.sqlite.JDBC");
3010    } catch(ClassNotFoundException ex) {
3011      ex.printStackTrace();
3012    }
3013    if(args.length > 3) {
3014      System.err.println("Usage: java DatabaseEditor <dbname> [<dbCharset> [<File System charset]]");
3015      return;
3016    }
3017    if(args.length > 2) {
3018      fileCharset = Charset.availableCharsets().get(args[1]);
3019      if(fileCharset == null) {
3020        System.err.println("Unknown charset: " + args[1]);
3021        System.err.println();
3022        System.err.println("Available charsets are: " +
3023                           Charset.availableCharsets().keySet());
3024        System.err.println();
3025        System.err.println("Default charset is: " + Charset.defaultCharset());
3026        return;
3027      }
3028    }
3029
3030    if(args.length > 1) {
3031      dbCharset = Charset.availableCharsets().get(args[1]);
3032      if(dbCharset == null) {
3033        System.err.println("Unknown charset: " + args[1]);
3034        System.err.println();
3035        System.err.println("Available charsets are: " +
3036                           Charset.availableCharsets().keySet());
3037        System.err.println();
3038        System.err.println("Default charset is: " + Charset.defaultCharset());
3039        return;
3040      }
3041    }
3042    String db = args.length > 0 ? args[0] : null;
3043    SwingUtilities.invokeLater(new Runnable() {
3044        public void run() {
3045          makeTableList(db);
3046        }
3047      });
3048  }
3049} // class DatabaseEditor extends JFrame