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;
17 import java.awt.event.*;
21 import java.util.Map.Entry;
22 import java.util.List;
25 import javax.swing.event.*;
26 import javax.swing.tree.*;
28 import net.argius.stew.*;
31 * The Database Information Tree is a tree pane that provides to
32 * display database object information from DatabaseMetaData.
34 final class DatabaseInfoTree extends JTree implements AnyActionListener, TextSearch {
41 generateUpdateStatement,
42 generateInsertStatement,
44 toggleShowColumnNumber
47 private static final Logger log = Logger.getLogger(DatabaseInfoTree.class);
48 private static final ResourceManager res = ResourceManager.getInstance(DatabaseInfoTree.class);
50 static volatile boolean showColumnNumber;
52 private Connector currentConnector;
53 private DatabaseMetaData dbmeta;
54 private AnyActionListener anyActionListener;
56 DatabaseInfoTree(AnyActionListener anyActionListener) {
57 this.anyActionListener = anyActionListener;
58 setRootVisible(false);
59 setShowsRootHandles(false);
60 setScrollsOnExpand(true);
61 setCellRenderer(new Renderer());
62 setModel(new DefaultTreeModel(null));
64 int sckm = Utilities.getMenuShortcutKeyMask();
65 AnyAction aa = new AnyAction(this);
66 aa.bindKeyStroke(false, copy, KeyStroke.getKeyStroke(VK_C, sckm));
67 aa.bindSelf(copySimpleName, getKeyStroke(VK_C, sckm | ALT_DOWN_MASK));
68 aa.bindSelf(copyFullName, getKeyStroke(VK_C, sckm | SHIFT_DOWN_MASK));
72 protected void processMouseEvent(MouseEvent e) {
73 super.processMouseEvent(e);
74 if (e.getID() == MouseEvent.MOUSE_CLICKED && e.getClickCount() % 2 == 0) {
75 anyActionPerformed(new AnyActionEvent(this, jumpToColumnByName));
80 public void anyActionPerformed(AnyActionEvent ev) {
81 log.atEnter("anyActionPerformed", ev);
82 if (ev.isAnyOf(copySimpleName)) {
84 } else if (ev.isAnyOf(copyFullName)) {
86 } else if (ev.isAnyOf(refresh)) {
87 for (TreePath path : getSelectionPaths()) {
88 refresh((InfoNode)path.getLastPathComponent());
90 } else if (ev.isAnyOf(generateWherePhrase)) {
91 List<ColumnNode> columnNodes = new ArrayList<ColumnNode>();
92 for (TreeNode node : getSelectionNodes()) {
93 if (node instanceof ColumnNode) {
94 columnNodes.add((ColumnNode)node);
97 final String phrase = generateEquivalentJoinClause(columnNodes);
98 if (phrase.length() > 0) {
99 insertTextIntoTextArea(addCommas(phrase));
101 } else if (ev.isAnyOf(generateSelectPhrase)) {
102 final String phrase = generateSelectPhrase(getSelectionNodes());
103 if (phrase.length() > 0) {
104 insertTextIntoTextArea(phrase + " WHERE ");
106 } else if (ev.isAnyOf(generateUpdateStatement, generateInsertStatement)) {
107 final boolean isInsert = ev.isAnyOf(generateInsertStatement);
109 final String phrase = generateUpdateOrInsertPhrase(getSelectionNodes(), isInsert);
110 if (phrase.length() > 0) {
112 insertTextIntoTextArea(addCommas(phrase));
114 insertTextIntoTextArea(phrase + " WHERE ");
117 } catch (IllegalArgumentException ex) {
118 showInformationMessageDialog(this, ex.getMessage(), "");
120 } else if (ev.isAnyOf(jumpToColumnByName)) {
121 jumpToColumnByName();
122 } else if (ev.isAnyOf(toggleShowColumnNumber)) {
123 showColumnNumber = !showColumnNumber;
126 log.warn("not expected: Event=%s", ev);
128 log.atExit("anyActionPerformed");
132 public TreePath[] getSelectionPaths() {
133 TreePath[] a = super.getSelectionPaths();
135 return new TreePath[0];
140 List<TreeNode> getSelectionNodes() {
141 List<TreeNode> a = new ArrayList<TreeNode>();
142 for (TreePath path : getSelectionPaths()) {
143 a.add((TreeNode)path.getLastPathComponent());
148 private void insertTextIntoTextArea(String s) {
149 AnyActionEvent ev = new AnyActionEvent(this, ConsoleTextArea.ActionKey.insertText, s);
150 anyActionListener.anyActionPerformed(ev);
153 private void copySimpleName() {
154 TreePath[] paths = getSelectionPaths();
155 if (paths == null || paths.length == 0) {
158 List<String> names = new ArrayList<String>(paths.length);
159 for (TreePath path : paths) {
163 Object o = path.getLastPathComponent();
164 assert o instanceof InfoNode;
166 if (o instanceof ColumnNode) {
167 name = ((ColumnNode)o).getName();
168 } else if (o instanceof TableNode) {
169 name = ((TableNode)o).getName();
175 ClipboardHelper.setStrings(names);
178 private void copyFullName() {
179 TreePath[] paths = getSelectionPaths();
180 if (paths == null || paths.length == 0) {
183 List<String> names = new ArrayList<String>(paths.length);
184 for (TreePath path : paths) {
188 Object o = path.getLastPathComponent();
189 assert o instanceof InfoNode;
190 names.add(((InfoNode)o).getNodeFullName());
192 ClipboardHelper.setStrings(names);
195 private void jumpToColumnByName() {
196 TreePath[] paths = getSelectionPaths();
197 if (paths == null || paths.length == 0) {
200 final TreePath path = paths[0];
201 Object o = path.getLastPathComponent();
202 if (o instanceof ColumnNode) {
203 ColumnNode node = (ColumnNode)o;
204 AnyActionEvent ev = new AnyActionEvent(this,
205 ResultSetTable.ActionKey.jumpToColumn,
207 anyActionListener.anyActionPerformed(ev);
211 private static String addCommas(String phrase) {
213 for (final char ch : phrase.toCharArray()) {
219 return String.format("%s;%s", phrase, join("", nCopies(c - 1, ",")));
224 static String generateEquivalentJoinClause(List<ColumnNode> nodes) {
225 if (nodes.isEmpty()) {
228 Set<String> tableNames = new LinkedHashSet<String>();
229 ListMap columnMap = new ListMap();
230 for (ColumnNode node : nodes) {
231 final String tableName = node.getTableNode().getNodeFullName();
232 final String columnName = node.getName();
233 tableNames.add(tableName);
234 columnMap.add(columnName, String.format("%s.%s", tableName, columnName));
236 assert tableNames.size() >= 1;
237 List<String> expressions = new ArrayList<String>();
238 if (tableNames.size() == 1) {
239 for (ColumnNode node : nodes) {
240 expressions.add(String.format("%s=?", node.getName()));
242 } else { // size >= 2
243 List<String> expressions2 = new ArrayList<String>();
244 for (Entry<String, List<String>> entry : columnMap.entrySet()) {
245 List<String> a = entry.getValue();
246 final int n = a.size();
248 expressions2.add(String.format("%s=?", a.get(0)));
250 for (int i = 0; i < n; i++) {
251 for (int j = i + 1; j < n; j++) {
252 expressions.add(String.format("%s=%s", a.get(i), a.get(j)));
257 expressions.addAll(expressions2);
259 return String.format("%s", join(" AND ", expressions));
262 static String generateSelectPhrase(List<TreeNode> nodes) {
263 Set<String> tableNames = new LinkedHashSet<String>();
264 ListMap columnMap = new ListMap();
265 for (TreeNode node : nodes) {
266 if (node instanceof TableNode) {
267 final String tableFullName = ((TableNode)node).getNodeFullName();
268 tableNames.add(tableFullName);
269 columnMap.add(tableFullName);
270 } else if (node instanceof ColumnNode) {
271 ColumnNode cn = (ColumnNode)node;
272 final String tableFullName = cn.getTableNode().getNodeFullName();
273 tableNames.add(tableFullName);
274 columnMap.add(tableFullName, cn.getNodeFullName());
277 if (tableNames.isEmpty()) {
280 List<String> columnNames = new ArrayList<String>();
281 if (tableNames.size() == 1) {
282 List<String> a = new ArrayList<String>();
283 for (TreeNode node : nodes) {
284 if (node instanceof ColumnNode) {
285 ColumnNode cn = (ColumnNode)node;
290 columnNames.add("*");
292 columnNames.addAll(a);
294 } else { // size >= 2
295 for (Entry<String, List<String>> entry : columnMap.entrySet()) {
296 final List<String> columnsInTable = entry.getValue();
297 if (columnsInTable.isEmpty()) {
298 columnNames.add(entry.getKey() + ".*");
300 columnNames.addAll(columnsInTable);
304 return String.format("SELECT %s FROM %s", join(", ", columnNames), join(", ", tableNames));
307 static String generateUpdateOrInsertPhrase(List<TreeNode> nodes, boolean isInsert) {
308 Set<String> tableNames = new LinkedHashSet<String>();
309 ListMap columnMap = new ListMap();
310 for (TreeNode node : nodes) {
311 if (node instanceof TableNode) {
312 final String tableFullName = ((TableNode)node).getNodeFullName();
313 tableNames.add(tableFullName);
314 columnMap.add(tableFullName);
315 } else if (node instanceof ColumnNode) {
316 ColumnNode cn = (ColumnNode)node;
317 final String tableFullName = cn.getTableNode().getNodeFullName();
318 tableNames.add(tableFullName);
319 columnMap.add(tableFullName, cn.getName());
322 if (tableNames.isEmpty()) {
325 if (tableNames.size() >= 2) {
326 throw new IllegalArgumentException(res.get("e.enables-select-just-1-table"));
328 final String tableName = join("", tableNames);
329 List<String> columnsInTable = columnMap.get(tableName);
330 if (columnsInTable.isEmpty()) {
332 List<TableNode> tableNodes = new ArrayList<TableNode>();
333 for (TreeNode node : nodes) {
334 if (node instanceof TableNode) {
335 tableNodes.add((TableNode)node);
339 TableNode tableNode = tableNodes.get(0);
340 if (tableNode.getChildCount() == 0) {
341 throw new IllegalArgumentException(res.get("i.can-only-use-after-tablenode-expanded"));
343 for (int i = 0, n = tableNode.getChildCount(); i < n; i++) {
344 ColumnNode child = (ColumnNode)tableNode.getChildAt(i);
345 columnsInTable.add(child.getName());
353 final int columnCount = columnsInTable.size();
354 phrase = String.format("INSERT INTO %s (%s) VALUES (%s)",
356 join(",", columnsInTable),
357 join(",", nCopies(columnCount, "?")));
359 List<String> columnExpressions = new ArrayList<String>();
360 for (final String columnName : columnsInTable) {
361 columnExpressions.add(columnName + "=?");
363 phrase = String.format("UPDATE %s SET %s", tableName, join(", ", columnExpressions));
371 public boolean search(Matcher matcher) {
372 return search(resolveTargetPath(getSelectionPath()), matcher);
375 private static TreePath resolveTargetPath(TreePath path) {
377 TreePath parent = path.getParentPath();
378 if (parent != null) {
385 private boolean search(TreePath path, Matcher matcher) {
389 TreeNode node = (TreeNode)path.getLastPathComponent();
393 boolean found = false;
394 found = matcher.find(node.toString());
396 addSelectionPath(path);
398 removeSelectionPath(path);
400 if (!node.isLeaf() && node.getChildCount() >= 0) {
401 @SuppressWarnings("unchecked")
402 Iterable<DefaultMutableTreeNode> children = Collections.list(node.children());
403 for (DefaultMutableTreeNode child : children) {
404 if (search(path.pathByAddingChild(child), matcher)) {
413 public void reset() {
420 * Refreshes the root and its children.
421 * @param env Environment
422 * @throws SQLException
424 void refreshRoot(Environment env) throws SQLException {
425 Connector c = env.getCurrentConnector();
427 if (log.isDebugEnabled()) {
428 log.debug("not connected");
430 currentConnector = null;
433 if (c == currentConnector && getModel().getRoot() != null) {
434 if (log.isDebugEnabled()) {
435 log.debug("not changed");
439 if (log.isDebugEnabled()) {
440 log.debug("updating");
442 // initializing models
443 ConnectorNode connectorNode = new ConnectorNode(c.getName());
444 DefaultTreeModel model = new DefaultTreeModel(connectorNode);
446 final DefaultTreeSelectionModel m = new DefaultTreeSelectionModel();
447 m.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
448 setSelectionModel(m);
449 // initializing nodes
450 final DatabaseMetaData dbmeta = env.getCurrentConnection().getMetaData();
451 final Set<InfoNode> createdStatusSet = new HashSet<InfoNode>();
452 expandNode(connectorNode, dbmeta);
453 createdStatusSet.add(connectorNode);
455 addTreeWillExpandListener(new TreeWillExpandListener() {
457 public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
458 TreePath path = event.getPath();
459 final Object lastPathComponent = path.getLastPathComponent();
460 if (!createdStatusSet.contains(lastPathComponent)) {
461 InfoNode node = (InfoNode)lastPathComponent;
465 createdStatusSet.add(node);
467 expandNode(node, dbmeta);
468 } catch (SQLException ex) {
469 throw new RuntimeException(ex);
474 public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
478 this.dbmeta = dbmeta;
481 setRootVisible(true);
482 this.currentConnector = c;
485 File confFile = Bootstrap.getSystemFile("autoexpansion.tsv");
486 if (confFile.exists() && confFile.length() > 0) {
487 AnyAction aa = new AnyAction(this);
488 Scanner r = new Scanner(confFile);
490 while (r.hasNextLine()) {
491 final String line = r.nextLine();
492 if (line.matches("^\\s*#.*")) {
495 aa.doParallel("expandNodes", Arrays.asList(line.split("\t")));
501 } catch (IOException ex) {
502 throw new IllegalStateException(ex);
506 void expandNodes(List<String> a) {
507 long startTime = System.currentTimeMillis();
508 AnyAction aa = new AnyAction(this);
510 while (index < a.size()) {
511 final String s = a.subList(0, index + 1).toString();
512 for (int i = 0, n = getRowCount(); i < n; i++) {
515 target = getPathForRow(i);
516 } catch (IndexOutOfBoundsException ex) {
517 // FIXME when IndexOutOfBoundsException was thrown at expandNodes
521 if (target != null && target.toString().equals(s)) {
522 if (!isExpanded(target)) {
523 aa.doLater("expandLater", target);
524 Utilities.sleep(200L);
530 if (System.currentTimeMillis() - startTime > 5000L) {
536 // called by expandNodes
537 @SuppressWarnings("unused")
538 private void expandLater(TreePath parent) {
543 * Refreshes a node and its children.
546 void refresh(InfoNode node) {
547 if (dbmeta == null) {
550 node.removeAllChildren();
551 final DefaultTreeModel model = (DefaultTreeModel)getModel();
554 expandNode(node, dbmeta);
555 } catch (SQLException ex) {
556 throw new RuntimeException(ex);
564 * @throws SQLException
566 void expandNode(final InfoNode parent, final DatabaseMetaData dbmeta) throws SQLException {
567 if (parent.isLeaf()) {
570 final DefaultTreeModel model = (DefaultTreeModel)getModel();
571 final InfoNode tmpNode = new InfoNode(res.get("i.paren-in-processing")) {
573 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
574 return Collections.emptyList();
577 public boolean isLeaf() {
581 invokeLater(new Runnable() {
584 model.insertNodeInto(tmpNode, parent, 0);
588 DaemonThreadFactory.execute(new Runnable() {
592 final List<InfoNode> children = new ArrayList<InfoNode>(parent.createChildren(dbmeta));
593 invokeLater(new Runnable() {
596 for (InfoNode child : children) {
597 model.insertNodeInto(child, parent, parent.getChildCount());
599 model.removeNodeFromParent(tmpNode);
602 } catch (SQLException ex) {
604 if (dbmeta.getConnection().isClosed())
606 } catch (SQLException exx) {
607 ex.setNextException(exx);
609 throw new RuntimeException(ex);
619 for (TreeWillExpandListener listener : getListeners(TreeWillExpandListener.class).clone()) {
620 removeTreeWillExpandListener(listener);
622 setModel(new DefaultTreeModel(null));
623 currentConnector = null;
625 if (log.isDebugEnabled()) {
626 log.debug("cleared");
632 private static final class ListMap extends LinkedHashMap<String, List<String>> {
638 void add(String key, String... values) {
639 if (get(key) == null) {
640 put(key, new ArrayList<String>());
642 for (String value : values) {
649 private static class Renderer extends DefaultTreeCellRenderer {
656 public Component getTreeCellRendererComponent(JTree tree,
663 super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
664 if (value instanceof InfoNode) {
665 setIcon(Utilities.getImageIcon(((InfoNode)value).getIconName()));
667 if (value instanceof ColumnNode) {
668 if (showColumnNumber) {
669 TreePath path = tree.getPathForRow(row);
671 TreePath parent = path.getParentPath();
672 if (parent != null) {
673 final int index = row - tree.getRowForPath(parent);
674 setText(String.format("%d %s", index, getText()));
684 private abstract static class InfoNode extends DefaultMutableTreeNode {
686 InfoNode(Object userObject) {
687 super(userObject, true);
691 public boolean isLeaf() {
695 abstract protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException;
697 String getIconName() {
698 final String className = getClass().getName();
699 final String nodeType = className.replaceFirst(".+?([^\\$]+)Node$", "$1");
700 return "node-" + nodeType.toLowerCase() + ".png";
703 protected String getNodeFullName() {
704 return String.valueOf(userObject);
707 static List<TableTypeNode> getTableTypeNodes(DatabaseMetaData dbmeta,
709 String schema) throws SQLException {
710 List<TableTypeNode> a = new ArrayList<TableTypeNode>();
711 ResultSet rs = dbmeta.getTableTypes();
714 TableTypeNode typeNode = new TableTypeNode(catalog, schema, rs.getString(1));
715 if (typeNode.hasItems(dbmeta)) {
723 a.add(new TableTypeNode(catalog, schema, "TABLE"));
730 private static class ConnectorNode extends InfoNode {
732 ConnectorNode(String name) {
737 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
738 List<InfoNode> a = new ArrayList<InfoNode>();
739 if (dbmeta.supportsCatalogsInDataManipulation()) {
740 ResultSet rs = dbmeta.getCatalogs();
743 a.add(new CatalogNode(rs.getString(1)));
748 } else if (dbmeta.supportsSchemasInDataManipulation()) {
749 ResultSet rs = dbmeta.getSchemas();
752 a.add(new SchemaNode(null, rs.getString(1)));
758 a.addAll(getTableTypeNodes(dbmeta, null, null));
765 private static final class CatalogNode extends InfoNode {
767 private final String name;
769 CatalogNode(String name) {
775 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
776 List<InfoNode> a = new ArrayList<InfoNode>();
777 if (dbmeta.supportsSchemasInDataManipulation()) {
778 ResultSet rs = dbmeta.getSchemas();
781 a.add(new SchemaNode(name, rs.getString(1)));
787 a.addAll(getTableTypeNodes(dbmeta, name, null));
794 private static final class SchemaNode extends InfoNode {
796 private final String catalog;
797 private final String schema;
799 SchemaNode(String catalog, String schema) {
801 this.catalog = catalog;
802 this.schema = schema;
806 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
807 List<InfoNode> a = new ArrayList<InfoNode>();
808 a.addAll(getTableTypeNodes(dbmeta, catalog, schema));
814 private static final class TableTypeNode extends InfoNode {
816 private static final String ICON_NAME_FORMAT = "node-tabletype-%s.png";
818 private final String catalog;
819 private final String schema;
820 private final String tableType;
822 TableTypeNode(String catalog, String schema, String tableType) {
824 this.catalog = catalog;
825 this.schema = schema;
826 this.tableType = tableType;
830 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
831 List<InfoNode> a = new ArrayList<InfoNode>();
832 ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
835 final String table = rs.getString(3);
836 final String type = rs.getString(4);
837 final boolean kindOfTable = type.matches("TABLE|VIEW|SYNONYM");
838 a.add(new TableNode(catalog, schema, table, kindOfTable));
847 String getIconName() {
848 final String name = String.format(ICON_NAME_FORMAT, getUserObject());
849 if (getClass().getResource("icon/" + name) == null) {
850 return String.format(ICON_NAME_FORMAT, "");
855 boolean hasItems(DatabaseMetaData dbmeta) throws SQLException {
856 ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
866 static final class TableNode extends InfoNode {
868 private final String catalog;
869 private final String schema;
870 private final String name;
871 private final boolean kindOfTable;
873 TableNode(String catalog, String schema, String name, boolean kindOfTable) {
875 this.catalog = catalog;
876 this.schema = schema;
878 this.kindOfTable = kindOfTable;
882 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
883 List<InfoNode> a = new ArrayList<InfoNode>();
884 ResultSet rs = dbmeta.getColumns(catalog, schema, name, null);
887 a.add(new ColumnNode(rs.getString(4),
900 public boolean isLeaf() {
905 protected String getNodeFullName() {
906 List<String> a = new ArrayList<String>();
907 if (catalog != null) {
910 if (schema != null) {
921 boolean isKindOfTable() {
927 static final class ColumnNode extends InfoNode {
929 private final String name;
930 private final TableNode tableNode;
932 ColumnNode(String name, String type, int size, String nulls, TableNode tableNode) {
933 super(format(name, type, size, nulls));
934 setAllowsChildren(false);
936 this.tableNode = tableNode;
943 TableNode getTableNode() {
947 private static String format(String name, String type, int size, String nulls) {
948 final String nonNull = "NO".equals(nulls) ? " NOT NULL" : "";
949 return String.format("%s [%s(%d)%s]", name, type, size, nonNull);
953 public boolean isLeaf() {
958 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
963 protected String getNodeFullName() {
964 return String.format("%s.%s", tableNode.getNodeFullName(), name);