1 package net.argius.stew.ui.window;
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;
16 import java.awt.event.*;
20 import java.util.Map.Entry;
21 import java.util.List;
23 import javax.swing.event.*;
24 import javax.swing.tree.*;
25 import net.argius.stew.*;
28 * The Database Information Tree is a tree pane that provides to
29 * display database object information from DatabaseMetaData.
31 final class DatabaseInfoTree extends JTree implements AnyActionListener, TextSearch {
38 generateUpdateStatement,
39 generateInsertStatement,
41 toggleShowColumnNumber
44 private static final Logger log = Logger.getLogger(DatabaseInfoTree.class);
45 private static final ResourceManager res = ResourceManager.getInstance(DatabaseInfoTree.class);
47 static volatile boolean showColumnNumber;
49 private Connector currentConnector;
50 private DatabaseMetaData dbmeta;
51 private AnyActionListener anyActionListener;
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));
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));
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));
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);
83 action.actionPerformed(new ActionEvent(this, 1001, cmd));
85 } else if (ev.isAnyOf(copySimpleName)) {
87 } else if (ev.isAnyOf(copyFullName)) {
89 } else if (ev.isAnyOf(refresh)) {
90 for (TreePath path : getSelectionPaths()) {
91 refresh((InfoNode)path.getLastPathComponent());
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);
100 final String phrase = generateEquivalentJoinClause(columnNodes);
101 if (phrase.length() > 0) {
102 insertTextIntoTextArea(addCommas(phrase));
104 } else if (ev.isAnyOf(generateSelectPhrase)) {
105 final String phrase = generateSelectPhrase(getSelectionNodes());
106 if (phrase.length() > 0) {
107 insertTextIntoTextArea(phrase + " WHERE ");
109 } else if (ev.isAnyOf(generateUpdateStatement, generateInsertStatement)) {
110 final boolean isInsert = ev.isAnyOf(generateInsertStatement);
112 final String phrase = generateUpdateOrInsertPhrase(getSelectionNodes(), isInsert);
113 if (phrase.length() > 0) {
115 insertTextIntoTextArea(addCommas(phrase));
117 insertTextIntoTextArea(phrase + " WHERE ");
120 } catch (IllegalArgumentException ex) {
121 showInformationMessageDialog(this, ex.getMessage(), "");
123 } else if (ev.isAnyOf(jumpToColumnByName)) {
124 jumpToColumnByName();
125 } else if (ev.isAnyOf(toggleShowColumnNumber)) {
126 showColumnNumber = !showColumnNumber;
129 log.warn("not expected: Event=%s", ev);
131 log.atExit("anyActionPerformed");
135 public TreePath[] getSelectionPaths() {
136 TreePath[] a = super.getSelectionPaths();
138 return new TreePath[0];
143 List<TreeNode> getSelectionNodes() {
144 List<TreeNode> a = new ArrayList<TreeNode>();
145 for (TreePath path : getSelectionPaths()) {
146 a.add((TreeNode)path.getLastPathComponent());
151 private void insertTextIntoTextArea(String s) {
152 AnyActionEvent ev = new AnyActionEvent(this, ConsoleTextArea.ActionKey.insertText, s);
153 anyActionListener.anyActionPerformed(ev);
156 private void copySimpleName() {
157 TreePath[] paths = getSelectionPaths();
158 if (paths == null || paths.length == 0) {
161 List<String> names = new ArrayList<String>(paths.length);
162 for (TreePath path : paths) {
166 Object o = path.getLastPathComponent();
167 assert o instanceof InfoNode;
169 if (o instanceof ColumnNode) {
170 name = ((ColumnNode)o).getName();
171 } else if (o instanceof TableNode) {
172 name = ((TableNode)o).getName();
178 ClipboardHelper.setStrings(names);
181 private void copyFullName() {
182 TreePath[] paths = getSelectionPaths();
183 if (paths == null || paths.length == 0) {
186 List<String> names = new ArrayList<String>(paths.length);
187 for (TreePath path : paths) {
191 Object o = path.getLastPathComponent();
192 assert o instanceof InfoNode;
193 names.add(((InfoNode)o).getNodeFullName());
195 ClipboardHelper.setStrings(names);
198 private void jumpToColumnByName() {
199 TreePath[] paths = getSelectionPaths();
200 if (paths == null || paths.length == 0) {
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,
210 anyActionListener.anyActionPerformed(ev);
214 private static String addCommas(String phrase) {
216 for (final char ch : phrase.toCharArray()) {
222 return String.format("%s;%s", phrase, join("", nCopies(c - 1, ",")));
227 static String generateEquivalentJoinClause(List<ColumnNode> nodes) {
228 if (nodes.isEmpty()) {
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));
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()));
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();
251 expressions2.add(String.format("%s=?", a.get(0)));
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)));
260 expressions.addAll(expressions2);
262 return String.format("%s", join(" AND ", expressions));
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());
280 if (tableNames.isEmpty()) {
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;
293 columnNames.add("*");
295 columnNames.addAll(a);
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() + ".*");
303 columnNames.addAll(columnsInTable);
307 return String.format("SELECT %s FROM %s", join(", ", columnNames), join(", ", tableNames));
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());
325 if (tableNames.isEmpty()) {
328 if (tableNames.size() >= 2) {
329 throw new IllegalArgumentException(res.get("e.enables-select-just-1-table"));
331 final String tableName = join("", tableNames);
332 List<String> columnsInTable = columnMap.get(tableName);
333 if (columnsInTable.isEmpty()) {
335 List<TableNode> tableNodes = new ArrayList<TableNode>();
336 for (TreeNode node : nodes) {
337 if (node instanceof TableNode) {
338 tableNodes.add((TableNode)node);
342 TableNode tableNode = tableNodes.get(0);
343 if (tableNode.getChildCount() == 0) {
344 throw new IllegalArgumentException(res.get("i.can-only-use-after-tablenode-expanded"));
346 for (int i = 0, n = tableNode.getChildCount(); i < n; i++) {
347 ColumnNode child = (ColumnNode)tableNode.getChildAt(i);
348 columnsInTable.add(child.getName());
356 final int columnCount = columnsInTable.size();
357 phrase = String.format("INSERT INTO %s (%s) VALUES (%s)",
359 join(",", columnsInTable),
360 join(",", nCopies(columnCount, "?")));
362 List<String> columnExpressions = new ArrayList<String>();
363 for (final String columnName : columnsInTable) {
364 columnExpressions.add(columnName + "=?");
366 phrase = String.format("UPDATE %s SET %s", tableName, join(", ", columnExpressions));
374 public boolean search(Matcher matcher) {
375 return search(resolveTargetPath(getSelectionPath()), matcher);
378 private static TreePath resolveTargetPath(TreePath path) {
380 TreePath parent = path.getParentPath();
381 if (parent != null) {
388 private boolean search(TreePath path, Matcher matcher) {
392 TreeNode node = (TreeNode)path.getLastPathComponent();
396 boolean found = false;
397 found = matcher.find(node.toString());
399 addSelectionPath(path);
401 removeSelectionPath(path);
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)) {
416 public void reset() {
423 * Refreshes the root and its children.
424 * @param env Environment
425 * @throws SQLException
427 void refreshRoot(Environment env) throws SQLException {
428 Connector c = env.getCurrentConnector();
430 if (log.isDebugEnabled()) {
431 log.debug("not connected");
433 currentConnector = null;
436 if (c == currentConnector && getModel().getRoot() != null) {
437 if (log.isDebugEnabled()) {
438 log.debug("not changed");
442 if (log.isDebugEnabled()) {
443 log.debug("updating");
445 // initializing models
446 ConnectorNode connectorNode = new ConnectorNode(c.getName());
447 DefaultTreeModel model = new DefaultTreeModel(connectorNode);
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);
458 addTreeWillExpandListener(new TreeWillExpandListener() {
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;
468 createdStatusSet.add(node);
470 expandNode(node, dbmeta);
471 } catch (SQLException ex) {
472 throw new RuntimeException(ex);
477 public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
481 this.dbmeta = dbmeta;
484 setRootVisible(true);
485 this.currentConnector = c;
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);
493 while (r.hasNextLine()) {
494 final String line = r.nextLine();
495 if (line.matches("^\\s*#.*")) {
498 aa.doParallel("expandNodes", Arrays.asList(line.split("\t")));
504 } catch (IOException ex) {
505 throw new IllegalStateException(ex);
509 void expandNodes(List<String> a) {
510 long startTime = System.currentTimeMillis();
511 AnyAction aa = new AnyAction(this);
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++) {
518 target = getPathForRow(i);
519 } catch (IndexOutOfBoundsException ex) {
520 // FIXME when IndexOutOfBoundsException was thrown at expandNodes
524 if (target != null && target.toString().equals(s)) {
525 if (!isExpanded(target)) {
526 aa.doLater("expandLater", target);
527 Utilities.sleep(200L);
533 if (System.currentTimeMillis() - startTime > 5000L) {
539 // called by expandNodes
540 @SuppressWarnings("unused")
541 private void expandLater(TreePath parent) {
546 * Refreshes a node and its children.
549 void refresh(InfoNode node) {
550 if (dbmeta == null) {
553 node.removeAllChildren();
554 final DefaultTreeModel model = (DefaultTreeModel)getModel();
557 expandNode(node, dbmeta);
558 } catch (SQLException ex) {
559 throw new RuntimeException(ex);
567 * @throws SQLException
569 void expandNode(final InfoNode parent, final DatabaseMetaData dbmeta) throws SQLException {
570 if (parent.isLeaf()) {
573 final DefaultTreeModel model = (DefaultTreeModel)getModel();
574 final InfoNode tmpNode = new InfoNode(res.get("i.paren-in-processing")) {
576 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
577 return Collections.emptyList();
580 public boolean isLeaf() {
584 invokeLater(new Runnable() {
587 model.insertNodeInto(tmpNode, parent, 0);
591 DaemonThreadFactory.execute(new Runnable() {
595 final List<InfoNode> children = new ArrayList<InfoNode>(parent.createChildren(dbmeta));
596 invokeLater(new Runnable() {
599 for (InfoNode child : children) {
600 model.insertNodeInto(child, parent, parent.getChildCount());
602 model.removeNodeFromParent(tmpNode);
605 } catch (SQLException ex) {
607 if (dbmeta.getConnection().isClosed())
609 } catch (SQLException exx) {
610 ex.setNextException(exx);
612 throw new RuntimeException(ex);
622 for (TreeWillExpandListener listener : getListeners(TreeWillExpandListener.class).clone()) {
623 removeTreeWillExpandListener(listener);
625 setModel(new DefaultTreeModel(null));
626 currentConnector = null;
628 if (log.isDebugEnabled()) {
629 log.debug("cleared");
635 private static final class ListMap extends LinkedHashMap<String, List<String>> {
641 void add(String key, String... values) {
642 if (get(key) == null) {
643 put(key, new ArrayList<String>());
645 for (String value : values) {
652 private static class Renderer extends DefaultTreeCellRenderer {
659 public Component getTreeCellRendererComponent(JTree tree,
666 super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
667 if (value instanceof InfoNode) {
668 setIcon(Utilities.getImageIcon(((InfoNode)value).getIconName()));
670 if (value instanceof ColumnNode) {
671 if (showColumnNumber) {
672 TreePath path = tree.getPathForRow(row);
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()));
687 private abstract static class InfoNode extends DefaultMutableTreeNode {
689 InfoNode(Object userObject) {
690 super(userObject, true);
694 public boolean isLeaf() {
698 abstract protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException;
700 String getIconName() {
701 final String className = getClass().getName();
702 final String nodeType = className.replaceFirst(".+?([^\\$]+)Node$", "$1");
703 return "node-" + nodeType.toLowerCase() + ".png";
706 protected String getNodeFullName() {
707 return String.valueOf(userObject);
710 static List<TableTypeNode> getTableTypeNodes(DatabaseMetaData dbmeta,
712 String schema) throws SQLException {
713 List<TableTypeNode> a = new ArrayList<TableTypeNode>();
714 ResultSet rs = dbmeta.getTableTypes();
717 TableTypeNode typeNode = new TableTypeNode(catalog, schema, rs.getString(1));
718 if (typeNode.hasItems(dbmeta)) {
726 a.add(new TableTypeNode(catalog, schema, "TABLE"));
733 private static class ConnectorNode extends InfoNode {
735 ConnectorNode(String name) {
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();
746 a.add(new CatalogNode(rs.getString(1)));
751 } else if (dbmeta.supportsSchemasInDataManipulation()) {
752 ResultSet rs = dbmeta.getSchemas();
755 a.add(new SchemaNode(null, rs.getString(1)));
761 a.addAll(getTableTypeNodes(dbmeta, null, null));
768 private static final class CatalogNode extends InfoNode {
770 private final String name;
772 CatalogNode(String name) {
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();
784 a.add(new SchemaNode(name, rs.getString(1)));
790 a.addAll(getTableTypeNodes(dbmeta, name, null));
797 private static final class SchemaNode extends InfoNode {
799 private final String catalog;
800 private final String schema;
802 SchemaNode(String catalog, String schema) {
804 this.catalog = catalog;
805 this.schema = schema;
809 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
810 List<InfoNode> a = new ArrayList<InfoNode>();
811 a.addAll(getTableTypeNodes(dbmeta, catalog, schema));
817 private static final class TableTypeNode extends InfoNode {
819 private static final String ICON_NAME_FORMAT = "node-tabletype-%s.png";
821 private final String catalog;
822 private final String schema;
823 private final String tableType;
825 TableTypeNode(String catalog, String schema, String tableType) {
827 this.catalog = catalog;
828 this.schema = schema;
829 this.tableType = tableType;
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});
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));
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, "");
858 boolean hasItems(DatabaseMetaData dbmeta) throws SQLException {
859 ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
869 static final class TableNode extends InfoNode {
871 private final String catalog;
872 private final String schema;
873 private final String name;
874 private final boolean kindOfTable;
876 TableNode(String catalog, String schema, String name, boolean kindOfTable) {
878 this.catalog = catalog;
879 this.schema = schema;
881 this.kindOfTable = kindOfTable;
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);
890 a.add(new ColumnNode(rs.getString(4),
903 public boolean isLeaf() {
908 protected String getNodeFullName() {
909 List<String> a = new ArrayList<String>();
910 if (catalog != null) {
913 if (schema != null) {
924 boolean isKindOfTable() {
930 static final class ColumnNode extends InfoNode {
932 private final String name;
933 private final TableNode tableNode;
935 ColumnNode(String name, String type, int size, String nulls, TableNode tableNode) {
936 super(format(name, type, size, nulls));
937 setAllowsChildren(false);
939 this.tableNode = tableNode;
946 TableNode getTableNode() {
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);
956 public boolean isLeaf() {
961 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
966 protected String getNodeFullName() {
967 return String.format("%s.%s", tableNode.getNodeFullName(), name);