OSDN Git Service

Merge branch 't32628' into v4.0.3
[stew/Stew4.git] / src / net / argius / stew / ui / window / DatabaseInfoTree.java
1 package net.argius.stew.ui.window;
2
3 import static java.awt.EventQueue.invokeLater;
4 import static java.awt.event.InputEvent.ALT_DOWN_MASK;
5 import static java.awt.event.InputEvent.SHIFT_DOWN_MASK;
6 import static java.awt.event.KeyEvent.VK_C;
7 import static java.util.Collections.emptyList;
8 import static java.util.Collections.nCopies;
9 import static javax.swing.KeyStroke.getKeyStroke;
10 import static net.argius.stew.text.TextUtilities.join;
11 import static net.argius.stew.ui.window.AnyActionKey.copy;
12 import static net.argius.stew.ui.window.AnyActionKey.refresh;
13 import static net.argius.stew.ui.window.DatabaseInfoTree.ActionKey.*;
14 import static net.argius.stew.ui.window.WindowOutputProcessor.showInformationMessageDialog;
15 import java.awt.*;
16 import java.awt.event.*;
17 import java.io.*;
18 import java.sql.*;
19 import java.util.*;
20 import java.util.Map.Entry;
21 import java.util.List;
22 import javax.swing.*;
23 import javax.swing.event.*;
24 import javax.swing.tree.*;
25 import net.argius.stew.*;
26
27 /**
28  * The Database Information Tree is a tree pane that provides to
29  * display database object information from DatabaseMetaData.
30  */
31 final class DatabaseInfoTree extends JTree implements AnyActionListener, TextSearch {
32
33     enum ActionKey {
34         copySimpleName,
35         copyFullName,
36         generateWherePhrase,
37         generateSelectPhrase,
38         generateUpdateStatement,
39         generateInsertStatement,
40         jumpToColumnByName,
41         toggleShowColumnNumber
42     }
43
44     private static final Logger log = Logger.getLogger(DatabaseInfoTree.class);
45     private static final ResourceManager res = ResourceManager.getInstance(DatabaseInfoTree.class);
46
47     static volatile boolean showColumnNumber;
48
49     private Connector currentConnector;
50     private DatabaseMetaData dbmeta;
51     private AnyActionListener anyActionListener;
52
53     DatabaseInfoTree(AnyActionListener anyActionListener) {
54         this.anyActionListener = anyActionListener;
55         setRootVisible(false);
56         setShowsRootHandles(false);
57         setScrollsOnExpand(true);
58         setCellRenderer(new Renderer());
59         setModel(new DefaultTreeModel(null));
60         // [Events]
61         int sckm = Utilities.getMenuShortcutKeyMask();
62         AnyAction aa = new AnyAction(this);
63         aa.bindKeyStroke(false, copy, KeyStroke.getKeyStroke(VK_C, sckm));
64         aa.bindSelf(copySimpleName, getKeyStroke(VK_C, sckm | ALT_DOWN_MASK));
65         aa.bindSelf(copyFullName, getKeyStroke(VK_C, sckm | SHIFT_DOWN_MASK));
66     }
67
68     @Override
69     protected void processMouseEvent(MouseEvent e) {
70         super.processMouseEvent(e);
71         if (e.getID() == MouseEvent.MOUSE_CLICKED && e.getClickCount() % 2 == 0) {
72             anyActionPerformed(new AnyActionEvent(this, jumpToColumnByName));
73         }
74     }
75
76     @Override
77     public void anyActionPerformed(AnyActionEvent ev) {
78         log.atEnter("anyActionPerformed", ev);
79         if (ev.isAnyOf(copy)) {
80             final String cmd = ev.getActionCommand();
81             Action action = getActionMap().get(cmd);
82             if (action != null) {
83                 action.actionPerformed(new ActionEvent(this, 1001, cmd));
84             }
85         } else if (ev.isAnyOf(copySimpleName)) {
86             copySimpleName();
87         } else if (ev.isAnyOf(copyFullName)) {
88             copyFullName();
89         } else if (ev.isAnyOf(refresh)) {
90             for (TreePath path : getSelectionPaths()) {
91                 refresh((InfoNode)path.getLastPathComponent());
92             }
93         } else if (ev.isAnyOf(generateWherePhrase)) {
94             List<ColumnNode> columnNodes = new ArrayList<ColumnNode>();
95             for (TreeNode node : getSelectionNodes()) {
96                 if (node instanceof ColumnNode) {
97                     columnNodes.add((ColumnNode)node);
98                 }
99             }
100             final String phrase = generateEquivalentJoinClause(columnNodes);
101             if (phrase.length() > 0) {
102                 insertTextIntoTextArea(addCommas(phrase));
103             }
104         } else if (ev.isAnyOf(generateSelectPhrase)) {
105             final String phrase = generateSelectPhrase(getSelectionNodes());
106             if (phrase.length() > 0) {
107                 insertTextIntoTextArea(phrase + " WHERE ");
108             }
109         } else if (ev.isAnyOf(generateUpdateStatement, generateInsertStatement)) {
110             final boolean isInsert = ev.isAnyOf(generateInsertStatement);
111             try {
112                 final String phrase = generateUpdateOrInsertPhrase(getSelectionNodes(), isInsert);
113                 if (phrase.length() > 0) {
114                     if (isInsert) {
115                         insertTextIntoTextArea(addCommas(phrase));
116                     } else {
117                         insertTextIntoTextArea(phrase + " WHERE ");
118                     }
119                 }
120             } catch (IllegalArgumentException ex) {
121                 showInformationMessageDialog(this, ex.getMessage(), "");
122             }
123         } else if (ev.isAnyOf(jumpToColumnByName)) {
124             jumpToColumnByName();
125         } else if (ev.isAnyOf(toggleShowColumnNumber)) {
126             showColumnNumber = !showColumnNumber;
127             repaint();
128         } else {
129             log.warn("not expected: Event=%s", ev);
130         }
131         log.atExit("anyActionPerformed");
132     }
133
134     @Override
135     public TreePath[] getSelectionPaths() {
136         TreePath[] a = super.getSelectionPaths();
137         if (a == null) {
138             return new TreePath[0];
139         }
140         return a;
141     }
142
143     List<TreeNode> getSelectionNodes() {
144         List<TreeNode> a = new ArrayList<TreeNode>();
145         for (TreePath path : getSelectionPaths()) {
146             a.add((TreeNode)path.getLastPathComponent());
147         }
148         return a;
149     }
150
151     private void insertTextIntoTextArea(String s) {
152         AnyActionEvent ev = new AnyActionEvent(this, ConsoleTextArea.ActionKey.insertText, s);
153         anyActionListener.anyActionPerformed(ev);
154     }
155
156     private void copySimpleName() {
157         TreePath[] paths = getSelectionPaths();
158         if (paths == null || paths.length == 0) {
159             return;
160         }
161         List<String> names = new ArrayList<String>(paths.length);
162         for (TreePath path : paths) {
163             if (path == null) {
164                 continue;
165             }
166             Object o = path.getLastPathComponent();
167             assert o instanceof InfoNode;
168             final String name;
169             if (o instanceof ColumnNode) {
170                 name = ((ColumnNode)o).getName();
171             } else if (o instanceof TableNode) {
172                 name = ((TableNode)o).getName();
173             } else {
174                 name = o.toString();
175             }
176             names.add(name);
177         }
178         ClipboardHelper.setStrings(names);
179     }
180
181     private void copyFullName() {
182         TreePath[] paths = getSelectionPaths();
183         if (paths == null || paths.length == 0) {
184             return;
185         }
186         List<String> names = new ArrayList<String>(paths.length);
187         for (TreePath path : paths) {
188             if (path == null) {
189                 continue;
190             }
191             Object o = path.getLastPathComponent();
192             assert o instanceof InfoNode;
193             names.add(((InfoNode)o).getNodeFullName());
194         }
195         ClipboardHelper.setStrings(names);
196     }
197
198     private void jumpToColumnByName() {
199         TreePath[] paths = getSelectionPaths();
200         if (paths == null || paths.length == 0) {
201             return;
202         }
203         final TreePath path = paths[0];
204         Object o = path.getLastPathComponent();
205         if (o instanceof ColumnNode) {
206             ColumnNode node = (ColumnNode)o;
207             AnyActionEvent ev = new AnyActionEvent(this,
208                                                    ResultSetTable.ActionKey.jumpToColumn,
209                                                    node.getName());
210             anyActionListener.anyActionPerformed(ev);
211         }
212     }
213
214     private static String addCommas(String phrase) {
215         int c = 0;
216         for (final char ch : phrase.toCharArray()) {
217             if (ch == '?') {
218                 ++c;
219             }
220         }
221         if (c >= 1) {
222             return String.format("%s;%s", phrase, join("", nCopies(c - 1, ",")));
223         }
224         return phrase;
225     }
226
227     static String generateEquivalentJoinClause(List<ColumnNode> nodes) {
228         if (nodes.isEmpty()) {
229             return "";
230         }
231         Set<String> tableNames = new LinkedHashSet<String>();
232         ListMap columnMap = new ListMap();
233         for (ColumnNode node : nodes) {
234             final String tableName = node.getTableNode().getNodeFullName();
235             final String columnName = node.getName();
236             tableNames.add(tableName);
237             columnMap.add(columnName, String.format("%s.%s", tableName, columnName));
238         }
239         assert tableNames.size() >= 1;
240         List<String> expressions = new ArrayList<String>();
241         if (tableNames.size() == 1) {
242             for (ColumnNode node : nodes) {
243                 expressions.add(String.format("%s=?", node.getName()));
244             }
245         } else { // size >= 2
246             List<String> expressions2 = new ArrayList<String>();
247             for (Entry<String, List<String>> entry : columnMap.entrySet()) {
248                 List<String> a = entry.getValue();
249                 final int n = a.size();
250                 assert n >= 1;
251                 expressions2.add(String.format("%s=?", a.get(0)));
252                 if (n >= 2) {
253                     for (int i = 0; i < n; i++) {
254                         for (int j = i + 1; j < n; j++) {
255                             expressions.add(String.format("%s=%s", a.get(i), a.get(j)));
256                         }
257                     }
258                 }
259             }
260             expressions.addAll(expressions2);
261         }
262         return String.format("%s", join(" AND ", expressions));
263     }
264
265     static String generateSelectPhrase(List<TreeNode> nodes) {
266         Set<String> tableNames = new LinkedHashSet<String>();
267         ListMap columnMap = new ListMap();
268         for (TreeNode node : nodes) {
269             if (node instanceof TableNode) {
270                 final String tableFullName = ((TableNode)node).getNodeFullName();
271                 tableNames.add(tableFullName);
272                 columnMap.add(tableFullName);
273             } else if (node instanceof ColumnNode) {
274                 ColumnNode cn = (ColumnNode)node;
275                 final String tableFullName = cn.getTableNode().getNodeFullName();
276                 tableNames.add(tableFullName);
277                 columnMap.add(tableFullName, cn.getNodeFullName());
278             }
279         }
280         if (tableNames.isEmpty()) {
281             return "";
282         }
283         List<String> columnNames = new ArrayList<String>();
284         if (tableNames.size() == 1) {
285             List<String> a = new ArrayList<String>();
286             for (TreeNode node : nodes) {
287                 if (node instanceof ColumnNode) {
288                     ColumnNode cn = (ColumnNode)node;
289                     a.add(cn.getName());
290                 }
291             }
292             if (a.isEmpty()) {
293                 columnNames.add("*");
294             } else {
295                 columnNames.addAll(a);
296             }
297         } else { // size >= 2
298             for (Entry<String, List<String>> entry : columnMap.entrySet()) {
299                 final List<String> columnsInTable = entry.getValue();
300                 if (columnsInTable.isEmpty()) {
301                     columnNames.add(entry.getKey() + ".*");
302                 } else {
303                     columnNames.addAll(columnsInTable);
304                 }
305             }
306         }
307         return String.format("SELECT %s FROM %s", join(", ", columnNames), join(", ", tableNames));
308     }
309
310     static String generateUpdateOrInsertPhrase(List<TreeNode> nodes, boolean isInsert) {
311         Set<String> tableNames = new LinkedHashSet<String>();
312         ListMap columnMap = new ListMap();
313         for (TreeNode node : nodes) {
314             if (node instanceof TableNode) {
315                 final String tableFullName = ((TableNode)node).getNodeFullName();
316                 tableNames.add(tableFullName);
317                 columnMap.add(tableFullName);
318             } else if (node instanceof ColumnNode) {
319                 ColumnNode cn = (ColumnNode)node;
320                 final String tableFullName = cn.getTableNode().getNodeFullName();
321                 tableNames.add(tableFullName);
322                 columnMap.add(tableFullName, cn.getName());
323             }
324         }
325         if (tableNames.isEmpty()) {
326             return "";
327         }
328         if (tableNames.size() >= 2) {
329             throw new IllegalArgumentException(res.get("e.enables-select-just-1-table"));
330         }
331         final String tableName = join("", tableNames);
332         List<String> columnsInTable = columnMap.get(tableName);
333         if (columnsInTable.isEmpty()) {
334             if (isInsert) {
335                 List<TableNode> tableNodes = new ArrayList<TableNode>();
336                 for (TreeNode node : nodes) {
337                     if (node instanceof TableNode) {
338                         tableNodes.add((TableNode)node);
339                         break;
340                     }
341                 }
342                 TableNode tableNode = tableNodes.get(0);
343                 if (tableNode.getChildCount() == 0) {
344                     throw new IllegalArgumentException(res.get("i.can-only-use-after-tablenode-expanded"));
345                 }
346                 for (int i = 0, n = tableNode.getChildCount(); i < n; i++) {
347                     ColumnNode child = (ColumnNode)tableNode.getChildAt(i);
348                     columnsInTable.add(child.getName());
349                 }
350             } else {
351                 return "";
352             }
353         }
354         final String phrase;
355         if (isInsert) {
356             final int columnCount = columnsInTable.size();
357             phrase = String.format("INSERT INTO %s (%s) VALUES (%s)",
358                                    tableName,
359                                    join(",", columnsInTable),
360                                    join(",", nCopies(columnCount, "?")));
361         } else {
362             List<String> columnExpressions = new ArrayList<String>();
363             for (final String columnName : columnsInTable) {
364                 columnExpressions.add(columnName + "=?");
365             }
366             phrase = String.format("UPDATE %s SET %s", tableName, join(", ", columnExpressions));
367         }
368         return phrase;
369     }
370
371     // text-search
372
373     @Override
374     public boolean search(Matcher matcher) {
375         return search(resolveTargetPath(getSelectionPath()), matcher);
376     }
377
378     private static TreePath resolveTargetPath(TreePath path) {
379         if (path != null) {
380             TreePath parent = path.getParentPath();
381             if (parent != null) {
382                 return parent;
383             }
384         }
385         return path;
386     }
387
388     private boolean search(TreePath path, Matcher matcher) {
389         if (path == null) {
390             return false;
391         }
392         TreeNode node = (TreeNode)path.getLastPathComponent();
393         if (node == null) {
394             return false;
395         }
396         boolean found = false;
397         found = matcher.find(node.toString());
398         if (found) {
399             addSelectionPath(path);
400         } else {
401             removeSelectionPath(path);
402         }
403         if (!node.isLeaf() && node.getChildCount() >= 0) {
404             @SuppressWarnings("unchecked")
405             Iterable<DefaultMutableTreeNode> children = Collections.list(node.children());
406             for (DefaultMutableTreeNode child : children) {
407                 if (search(path.pathByAddingChild(child), matcher)) {
408                     found = true;
409                 }
410             }
411         }
412         return found;
413     }
414
415     @Override
416     public void reset() {
417         // empty
418     }
419
420     // node expansion
421
422     /**
423      * Refreshes the root and its children.
424      * @param env Environment
425      * @throws SQLException
426      */
427     void refreshRoot(Environment env) throws SQLException {
428         Connector c = env.getCurrentConnector();
429         if (c == null) {
430             if (log.isDebugEnabled()) {
431                 log.debug("not connected");
432             }
433             currentConnector = null;
434             return;
435         }
436         if (c == currentConnector && getModel().getRoot() != null) {
437             if (log.isDebugEnabled()) {
438                 log.debug("not changed");
439             }
440             return;
441         }
442         if (log.isDebugEnabled()) {
443             log.debug("updating");
444         }
445         // initializing models
446         ConnectorNode connectorNode = new ConnectorNode(c.getName());
447         DefaultTreeModel model = new DefaultTreeModel(connectorNode);
448         setModel(model);
449         final DefaultTreeSelectionModel m = new DefaultTreeSelectionModel();
450         m.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
451         setSelectionModel(m);
452         // initializing nodes
453         final DatabaseMetaData dbmeta = env.getCurrentConnection().getMetaData();
454         final Set<InfoNode> createdStatusSet = new HashSet<InfoNode>();
455         expandNode(connectorNode, dbmeta);
456         createdStatusSet.add(connectorNode);
457         // events
458         addTreeWillExpandListener(new TreeWillExpandListener() {
459             @Override
460             public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
461                 TreePath path = event.getPath();
462                 final Object lastPathComponent = path.getLastPathComponent();
463                 if (!createdStatusSet.contains(lastPathComponent)) {
464                     InfoNode node = (InfoNode)lastPathComponent;
465                     if (node.isLeaf()) {
466                         return;
467                     }
468                     createdStatusSet.add(node);
469                     try {
470                         expandNode(node, dbmeta);
471                     } catch (SQLException ex) {
472                         throw new RuntimeException(ex);
473                     }
474                 }
475             }
476             @Override
477             public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
478                 // ignore
479             }
480         });
481         this.dbmeta = dbmeta;
482         // showing
483         model.reload();
484         setRootVisible(true);
485         this.currentConnector = c;
486         // auto-expansion
487         try {
488             File confFile = Bootstrap.getSystemFile("autoexpansion.tsv");
489             if (confFile.exists() && confFile.length() > 0) {
490                 AnyAction aa = new AnyAction(this);
491                 Scanner r = new Scanner(confFile);
492                 try {
493                     while (r.hasNextLine()) {
494                         final String line = r.nextLine();
495                         if (line.matches("^\\s*#.*")) {
496                             continue;
497                         }
498                         aa.doParallel("expandNodes", Arrays.asList(line.split("\t")));
499                     }
500                 } finally {
501                     r.close();
502                 }
503             }
504         } catch (IOException ex) {
505             throw new IllegalStateException(ex);
506         }
507     }
508
509     void expandNodes(List<String> a) {
510         long startTime = System.currentTimeMillis();
511         AnyAction aa = new AnyAction(this);
512         int index = 1;
513         while (index < a.size()) {
514             final String s = a.subList(0, index + 1).toString();
515             for (int i = 0, n = getRowCount(); i < n; i++) {
516                 TreePath target;
517                 try {
518                     target = getPathForRow(i);
519                 } catch (IndexOutOfBoundsException ex) {
520                     // FIXME when IndexOutOfBoundsException was thrown at expandNodes
521                     log.warn(ex);
522                     break;
523                 }
524                 if (target != null && target.toString().equals(s)) {
525                     if (!isExpanded(target)) {
526                         aa.doLater("expandLater", target);
527                         Utilities.sleep(200L);
528                     }
529                     index++;
530                     break;
531                 }
532             }
533             if (System.currentTimeMillis() - startTime > 5000L) {
534                 break; // timeout
535             }
536         }
537     }
538
539     // called by expandNodes
540     @SuppressWarnings("unused")
541     private void expandLater(TreePath parent) {
542         expandPath(parent);
543     }
544
545     /**
546      * Refreshes a node and its children.
547      * @param node
548      */
549     void refresh(InfoNode node) {
550         if (dbmeta == null) {
551             return;
552         }
553         node.removeAllChildren();
554         final DefaultTreeModel model = (DefaultTreeModel)getModel();
555         model.reload(node);
556         try {
557             expandNode(node, dbmeta);
558         } catch (SQLException ex) {
559             throw new RuntimeException(ex);
560         }
561     }
562
563     /**
564      * Expands a node.
565      * @param parent
566      * @param dbmeta
567      * @throws SQLException
568      */
569     void expandNode(final InfoNode parent, final DatabaseMetaData dbmeta) throws SQLException {
570         if (parent.isLeaf()) {
571             return;
572         }
573         final DefaultTreeModel model = (DefaultTreeModel)getModel();
574         final InfoNode tmpNode = new InfoNode(res.get("i.paren-in-processing")) {
575             @Override
576             protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
577                 return Collections.emptyList();
578             }
579             @Override
580             public boolean isLeaf() {
581                 return true;
582             }
583         };
584         invokeLater(new Runnable() {
585             @Override
586             public void run() {
587                 model.insertNodeInto(tmpNode, parent, 0);
588             }
589         });
590         // asynchronous
591         DaemonThreadFactory.execute(new Runnable() {
592             @Override
593             public void run() {
594                 try {
595                     final List<InfoNode> children = new ArrayList<InfoNode>(parent.createChildren(dbmeta));
596                     invokeLater(new Runnable() {
597                         @Override
598                         public void run() {
599                             for (InfoNode child : children) {
600                                 model.insertNodeInto(child, parent, parent.getChildCount());
601                             }
602                             model.removeNodeFromParent(tmpNode);
603                         }
604                     });
605                 } catch (SQLException ex) {
606                     try {
607                         if (dbmeta.getConnection().isClosed())
608                             return;
609                     } catch (SQLException exx) {
610                         ex.setNextException(exx);
611                     }
612                     throw new RuntimeException(ex);
613                 }
614             }
615         });
616     }
617
618     /**
619      * Clears (root).
620      */
621     void clear() {
622         for (TreeWillExpandListener listener : getListeners(TreeWillExpandListener.class).clone()) {
623             removeTreeWillExpandListener(listener);
624         }
625         setModel(new DefaultTreeModel(null));
626         currentConnector = null;
627         dbmeta = null;
628         if (log.isDebugEnabled()) {
629             log.debug("cleared");
630         }
631     }
632
633     // subclasses
634
635     private static final class ListMap extends LinkedHashMap<String, List<String>> {
636
637         ListMap() {
638             // empty
639         }
640
641         void add(String key, String... values) {
642             if (get(key) == null) {
643                 put(key, new ArrayList<String>());
644             }
645             for (String value : values) {
646                 get(key).add(value);
647             }
648         }
649
650     }
651
652     private static class Renderer extends DefaultTreeCellRenderer {
653
654         Renderer() {
655             // empty
656         }
657
658         @Override
659         public Component getTreeCellRendererComponent(JTree tree,
660                                                       Object value,
661                                                       boolean sel,
662                                                       boolean expanded,
663                                                       boolean leaf,
664                                                       int row,
665                                                       boolean hasFocus) {
666             super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
667             if (value instanceof InfoNode) {
668                 setIcon(Utilities.getImageIcon(((InfoNode)value).getIconName()));
669             }
670             if (value instanceof ColumnNode) {
671                 if (showColumnNumber) {
672                     TreePath path = tree.getPathForRow(row);
673                     if (path != null) {
674                         TreePath parent = path.getParentPath();
675                         if (parent != null) {
676                             final int index = row - tree.getRowForPath(parent);
677                             setText(String.format("%d %s", index, getText()));
678                         }
679                     }
680                 }
681             }
682             return this;
683         }
684
685     }
686
687     private abstract static class InfoNode extends DefaultMutableTreeNode {
688
689         InfoNode(Object userObject) {
690             super(userObject, true);
691         }
692
693         @Override
694         public boolean isLeaf() {
695             return false;
696         }
697
698         abstract protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException;
699
700         String getIconName() {
701             final String className = getClass().getName();
702             final String nodeType = className.replaceFirst(".+?([^\\$]+)Node$", "$1");
703             return "node-" + nodeType.toLowerCase() + ".png";
704         }
705
706         protected String getNodeFullName() {
707             return String.valueOf(userObject);
708         }
709
710         static List<TableTypeNode> getTableTypeNodes(DatabaseMetaData dbmeta,
711                                                      String catalog,
712                                                      String schema) throws SQLException {
713             List<TableTypeNode> a = new ArrayList<TableTypeNode>();
714             ResultSet rs = dbmeta.getTableTypes();
715             try {
716                 while (rs.next()) {
717                     TableTypeNode typeNode = new TableTypeNode(catalog, schema, rs.getString(1));
718                     if (typeNode.hasItems(dbmeta)) {
719                         a.add(typeNode);
720                     }
721                 }
722             } finally {
723                 rs.close();
724             }
725             if (a.isEmpty()) {
726                 a.add(new TableTypeNode(catalog, schema, "TABLE"));
727             }
728             return a;
729         }
730
731     }
732
733     private static class ConnectorNode extends InfoNode {
734
735         ConnectorNode(String name) {
736             super(name);
737         }
738
739         @Override
740         protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
741             List<InfoNode> a = new ArrayList<InfoNode>();
742             if (dbmeta.supportsCatalogsInDataManipulation()) {
743                 ResultSet rs = dbmeta.getCatalogs();
744                 try {
745                     while (rs.next()) {
746                         a.add(new CatalogNode(rs.getString(1)));
747                     }
748                 } finally {
749                     rs.close();
750                 }
751             } else if (dbmeta.supportsSchemasInDataManipulation()) {
752                 ResultSet rs = dbmeta.getSchemas();
753                 try {
754                     while (rs.next()) {
755                         a.add(new SchemaNode(null, rs.getString(1)));
756                     }
757                 } finally {
758                     rs.close();
759                 }
760             } else {
761                 a.addAll(getTableTypeNodes(dbmeta, null, null));
762             }
763             return a;
764         }
765
766     }
767
768     private static final class CatalogNode extends InfoNode {
769
770         private final String name;
771
772         CatalogNode(String name) {
773             super(name);
774             this.name = name;
775         }
776
777         @Override
778         protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
779             List<InfoNode> a = new ArrayList<InfoNode>();
780             if (dbmeta.supportsSchemasInDataManipulation()) {
781                 ResultSet rs = dbmeta.getSchemas();
782                 try {
783                     while (rs.next()) {
784                         a.add(new SchemaNode(name, rs.getString(1)));
785                     }
786                 } finally {
787                     rs.close();
788                 }
789             } else {
790                 a.addAll(getTableTypeNodes(dbmeta, name, null));
791             }
792             return a;
793         }
794
795     }
796
797     private static final class SchemaNode extends InfoNode {
798
799         private final String catalog;
800         private final String schema;
801
802         SchemaNode(String catalog, String schema) {
803             super(schema);
804             this.catalog = catalog;
805             this.schema = schema;
806         }
807
808         @Override
809         protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
810             List<InfoNode> a = new ArrayList<InfoNode>();
811             a.addAll(getTableTypeNodes(dbmeta, catalog, schema));
812             return a;
813         }
814
815     }
816
817     private static final class TableTypeNode extends InfoNode {
818
819         private static final String ICON_NAME_FORMAT = "node-tabletype-%s.png";
820
821         private final String catalog;
822         private final String schema;
823         private final String tableType;
824
825         TableTypeNode(String catalog, String schema, String tableType) {
826             super(tableType);
827             this.catalog = catalog;
828             this.schema = schema;
829             this.tableType = tableType;
830         }
831
832         @Override
833         protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
834             List<InfoNode> a = new ArrayList<InfoNode>();
835             ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
836             try {
837                 while (rs.next()) {
838                     final String table = rs.getString(3);
839                     final String type = rs.getString(4);
840                     final boolean kindOfTable = type.matches("TABLE|VIEW|SYNONYM");
841                     a.add(new TableNode(catalog, schema, table, kindOfTable));
842                 }
843             } finally {
844                 rs.close();
845             }
846             return a;
847         }
848
849         @Override
850         String getIconName() {
851             final String name = String.format(ICON_NAME_FORMAT, getUserObject());
852             if (getClass().getResource("icon/" + name) == null) {
853                 return String.format(ICON_NAME_FORMAT, "");
854             }
855             return name;
856         }
857
858         boolean hasItems(DatabaseMetaData dbmeta) throws SQLException {
859             ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
860             try {
861                 return rs.next();
862             } finally {
863                 rs.close();
864             }
865         }
866
867     }
868
869     static final class TableNode extends InfoNode {
870
871         private final String catalog;
872         private final String schema;
873         private final String name;
874         private final boolean kindOfTable;
875
876         TableNode(String catalog, String schema, String name, boolean kindOfTable) {
877             super(name);
878             this.catalog = catalog;
879             this.schema = schema;
880             this.name = name;
881             this.kindOfTable = kindOfTable;
882         }
883
884         @Override
885         protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
886             List<InfoNode> a = new ArrayList<InfoNode>();
887             ResultSet rs = dbmeta.getColumns(catalog, schema, name, null);
888             try {
889                 while (rs.next()) {
890                     a.add(new ColumnNode(rs.getString(4),
891                                          rs.getString(6),
892                                          rs.getInt(7),
893                                          rs.getString(18),
894                                          this));
895                 }
896             } finally {
897                 rs.close();
898             }
899             return a;
900         }
901
902         @Override
903         public boolean isLeaf() {
904             return false;
905         }
906
907         @Override
908         protected String getNodeFullName() {
909             List<String> a = new ArrayList<String>();
910             if (catalog != null) {
911                 a.add(catalog);
912             }
913             if (schema != null) {
914                 a.add(schema);
915             }
916             a.add(name);
917             return join(".", a);
918         }
919
920         String getName() {
921             return name;
922         }
923
924         boolean isKindOfTable() {
925             return kindOfTable;
926         }
927
928     }
929
930     static final class ColumnNode extends InfoNode {
931
932         private final String name;
933         private final TableNode tableNode;
934
935         ColumnNode(String name, String type, int size, String nulls, TableNode tableNode) {
936             super(format(name, type, size, nulls));
937             setAllowsChildren(false);
938             this.name = name;
939             this.tableNode = tableNode;
940         }
941
942         String getName() {
943             return name;
944         }
945
946         TableNode getTableNode() {
947             return tableNode;
948         }
949
950         private static String format(String name, String type, int size, String nulls) {
951             final String nonNull = "NO".equals(nulls) ? " NOT NULL" : "";
952             return String.format("%s [%s(%d)%s]", name, type, size, nonNull);
953         }
954
955         @Override
956         public boolean isLeaf() {
957             return true;
958         }
959
960         @Override
961         protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
962             return emptyList();
963         }
964
965         @Override
966         protected String getNodeFullName() {
967             return String.format("%s.%s", tableNode.getNodeFullName(), name);
968         }
969
970     }
971
972 }