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.ui.window.AnyActionKey.copy;
11 import static net.argius.stew.ui.window.AnyActionKey.refresh;
12 import static net.argius.stew.ui.window.DatabaseInfoTree.ActionKey.*;
13 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;
24 import javax.swing.event.*;
25 import javax.swing.tree.*;
27 import net.argius.stew.*;
28 import net.argius.stew.text.*;
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 TreePath[] paths = getSelectionPaths();
92 List<ColumnNode> columns = collectColumnNode(paths);
93 String[] tableNames = collectTableName(columns);
94 if (tableNames.length != 0) {
95 insertTextToTextArea(generateEquivalentJoinClause(columns));
97 } else if (ev.isAnyOf(generateSelectPhrase)) {
98 TreePath[] paths = getSelectionPaths();
99 List<ColumnNode> columns = collectColumnNode(paths);
100 String[] tableNames = collectTableName(columns);
101 if (tableNames.length != 0) {
102 List<String> columnNames = new ArrayList<String>();
103 final boolean one = tableNames.length == 1;
104 for (ColumnNode node : columns) {
105 columnNames.add(one ? node.getName() : node.getNodeFullName());
107 final String columnString = (columnNames.isEmpty())
109 : TextUtilities.join(", ", columnNames);
110 final String tableString = joinByComma(Arrays.asList(tableNames));
111 insertTextToTextArea(String.format("SELECT %s FROM %s WHERE ", columnString, tableString));
113 } else if (ev.isAnyOf(generateUpdateStatement)) {
114 TreePath[] paths = getSelectionPaths();
115 List<ColumnNode> columns = collectColumnNode(paths);
116 String[] tableNames = collectTableName(columns);
117 if (tableNames.length == 0) {
120 if (tableNames.length != 1 || columns.isEmpty()) {
121 showInformationMessageDialog(this, res.get("e.enables-select-just-1-table"), "");
123 final String tableName = tableNames[0];
124 List<String> columnExpressions = new ArrayList<String>();
125 for (ColumnNode columnNode : columns) {
126 columnExpressions.add(columnNode.getName() + "=?");
128 insertTextToTextArea(String.format("UPDATE %s SET %s WHERE ",
130 joinByComma(columnExpressions)));
132 } else if (ev.isAnyOf(generateInsertStatement)) {
133 generateInsertStatement();
134 } else if (ev.isAnyOf(jumpToColumnByName)) {
135 jumpToColumnByName();
136 } else if (ev.isAnyOf(toggleShowColumnNumber)) {
137 showColumnNumber = !showColumnNumber;
140 log.warn("not expected: Event=%s", ev);
142 log.atExit("anyActionPerformed");
145 private void insertTextToTextArea(String s) {
146 AnyActionEvent ev = new AnyActionEvent(this, ConsoleTextArea.ActionKey.insertText, s);
147 anyActionListener.anyActionPerformed(ev);
150 private static String joinByComma(List<?> a) {
151 StringBuilder buffer = new StringBuilder();
152 for (int i = 0, n = a.size(); i < n; i++) {
156 buffer.append(a.get(i));
158 return buffer.toString();
161 private void copySimpleName() {
162 TreePath[] paths = getSelectionPaths();
163 if (paths == null || paths.length == 0) {
166 List<String> names = new ArrayList<String>(paths.length);
167 for (TreePath path : paths) {
171 Object o = path.getLastPathComponent();
172 assert o instanceof InfoNode;
174 if (o instanceof ColumnNode) {
175 name = ((ColumnNode)o).getName();
176 } else if (o instanceof TableNode) {
177 name = ((TableNode)o).getName();
183 ClipboardHelper.setStrings(names);
186 private void copyFullName() {
187 TreePath[] paths = getSelectionPaths();
188 if (paths == null || paths.length == 0) {
191 List<String> names = new ArrayList<String>(paths.length);
192 for (TreePath path : paths) {
196 Object o = path.getLastPathComponent();
197 assert o instanceof InfoNode;
198 names.add(((InfoNode)o).getNodeFullName());
200 ClipboardHelper.setStrings(names);
203 private static List<ColumnNode> collectColumnNode(TreePath[] paths) {
204 List<ColumnNode> a = new ArrayList<ColumnNode>();
206 for (TreePath path : paths) {
207 InfoNode node = (InfoNode)path.getLastPathComponent();
208 if (node instanceof ColumnNode) {
209 ColumnNode columnNode = (ColumnNode)node;
217 private static List<TableNode> collectTableNode(TreePath[] paths) {
218 List<TableNode> a = new ArrayList<TableNode>();
220 for (TreePath path : paths) {
221 InfoNode node = (InfoNode)path.getLastPathComponent();
222 if (node instanceof TableNode) {
223 TableNode columnNode = (TableNode)node;
231 private static String[] collectTableName(List<ColumnNode> columnNodes) {
232 Set<String> tableNames = new LinkedHashSet<String>();
233 for (final ColumnNode node : columnNodes) {
234 tableNames.add(node.getTableNode().getNodeFullName());
236 return tableNames.toArray(new String[tableNames.size()]);
239 private void generateInsertStatement() {
240 TreePath[] paths = getSelectionPaths();
241 List<ColumnNode> columns = collectColumnNode(paths);
242 final String tableName;
243 final int tableCount;
244 if (columns.isEmpty()) {
245 List<TableNode> tables = collectTableNode(paths);
246 if (tables.isEmpty()) {
249 TableNode tableNode = tables.get(0);
250 if (tableNode.getChildCount() == 0) {
251 showInformationMessageDialog(this,
252 res.get("i.can-only-use-after-tablenode-expanded"),
256 @SuppressWarnings("unchecked")
257 List<ColumnNode> list = Collections.list(tableNode.children());
258 columns.addAll(list);
259 tableName = tableNode.getNodeFullName();
260 tableCount = tables.size();
262 String[] tableNames = collectTableName(columns);
263 tableCount = tableNames.length;
264 if (tableCount == 0) {
267 tableName = tableNames[0];
269 if (tableCount != 1) {
270 showInformationMessageDialog(this, res.get("e.enables-select-just-1-table"), "");
273 List<String> columnNames = new ArrayList<String>();
274 for (ColumnNode node : columns) {
275 columnNames.add(node.getName());
277 final int columnCount = columnNames.size();
278 insertTextToTextArea(String.format("INSERT INTO %s (%s) VALUES (%s);%s",
280 joinByComma(columnNames),
281 joinByComma(nCopies(columnCount, "?")),
282 TextUtilities.join(",", nCopies(columnCount, ""))));
285 private void jumpToColumnByName() {
286 TreePath[] paths = getSelectionPaths();
287 if (paths == null || paths.length == 0) {
290 final TreePath path = paths[0];
291 Object o = path.getLastPathComponent();
292 if (o instanceof ColumnNode) {
293 ColumnNode node = (ColumnNode)o;
294 AnyActionEvent ev = new AnyActionEvent(this,
295 ResultSetTable.ActionKey.jumpToColumn,
297 anyActionListener.anyActionPerformed(ev);
302 public boolean search(Matcher matcher) {
303 return search(resolveTargetPath(getSelectionPath()), matcher);
306 private static TreePath resolveTargetPath(TreePath path) {
308 TreePath parent = path.getParentPath();
309 if (parent != null) {
316 private boolean search(TreePath path, Matcher matcher) {
320 TreeNode node = (TreeNode)path.getLastPathComponent();
324 boolean found = false;
325 found = matcher.find(node.toString());
327 addSelectionPath(path);
329 removeSelectionPath(path);
331 if (!node.isLeaf() && node.getChildCount() >= 0) {
332 @SuppressWarnings("unchecked")
333 Iterable<DefaultMutableTreeNode> children = Collections.list(node.children());
334 for (DefaultMutableTreeNode child : children) {
335 if (search(path.pathByAddingChild(child), matcher)) {
344 public void reset() {
349 * Refreshes the root and its children.
350 * @param env Environment
351 * @throws SQLException
353 void refreshRoot(Environment env) throws SQLException {
354 Connector c = env.getCurrentConnector();
356 if (log.isDebugEnabled()) {
357 log.debug("not connected");
359 currentConnector = null;
362 if (c == currentConnector && getModel().getRoot() != null) {
363 if (log.isDebugEnabled()) {
364 log.debug("not changed");
368 if (log.isDebugEnabled()) {
369 log.debug("updating");
371 // initializing models
372 ConnectorNode connectorNode = new ConnectorNode(c.getName());
373 DefaultTreeModel model = new DefaultTreeModel(connectorNode);
375 final DefaultTreeSelectionModel m = new DefaultTreeSelectionModel();
376 m.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
377 setSelectionModel(m);
378 // initializing nodes
379 final DatabaseMetaData dbmeta = env.getCurrentConnection().getMetaData();
380 final Set<InfoNode> createdStatusSet = new HashSet<InfoNode>();
381 expandNode(connectorNode, dbmeta);
382 createdStatusSet.add(connectorNode);
384 addTreeWillExpandListener(new TreeWillExpandListener() {
386 public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
387 TreePath path = event.getPath();
388 final Object lastPathComponent = path.getLastPathComponent();
389 if (!createdStatusSet.contains(lastPathComponent)) {
390 InfoNode node = (InfoNode)lastPathComponent;
394 createdStatusSet.add(node);
396 expandNode(node, dbmeta);
397 } catch (SQLException ex) {
398 throw new RuntimeException(ex);
403 public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
407 this.dbmeta = dbmeta;
410 setRootVisible(true);
411 this.currentConnector = c;
414 File confFile = new File(Bootstrap.getDirectory(), "autoexpansion.tsv");
415 if (confFile.exists() && confFile.length() > 0) {
416 AnyAction aa = new AnyAction(this);
417 Scanner r = new Scanner(confFile);
419 while (r.hasNextLine()) {
420 final String line = r.nextLine();
421 if (line.matches("^\\s*#.*")) {
424 aa.doParallel("expandNodes", Arrays.asList(line.split("\t")));
430 } catch (IOException ex) {
431 throw new IllegalStateException(ex);
435 void expandNodes(List<String> a) {
436 long startTime = System.currentTimeMillis();
437 AnyAction aa = new AnyAction(this);
439 while (index < a.size()) {
440 final String s = a.subList(0, index + 1).toString();
441 for (int i = 0, n = getRowCount(); i < n; i++) {
444 target = getPathForRow(i);
445 } catch (IndexOutOfBoundsException ex) {
446 // FIXME when IndexOutOfBoundsException was thrown at expandNodes
450 if (target != null && target.toString().equals(s)) {
451 if (!isExpanded(target)) {
452 aa.doLater("expandLater", target);
453 Utilities.sleep(200L);
459 if (System.currentTimeMillis() - startTime > 5000L) {
465 // called by expandNodes
466 @SuppressWarnings("unused")
467 private void expandLater(TreePath parent) {
472 * Refreshes a node and its children.
475 void refresh(InfoNode node) {
476 if (dbmeta == null) {
479 node.removeAllChildren();
480 final DefaultTreeModel model = (DefaultTreeModel)getModel();
483 expandNode(node, dbmeta);
484 } catch (SQLException ex) {
485 throw new RuntimeException(ex);
493 * @throws SQLException
495 void expandNode(final InfoNode parent, final DatabaseMetaData dbmeta) throws SQLException {
496 if (parent.isLeaf()) {
499 final DefaultTreeModel model = (DefaultTreeModel)getModel();
500 final InfoNode tmpNode = new InfoNode(res.get("i.paren-in-processing")) {
502 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
503 return Collections.emptyList();
506 public boolean isLeaf() {
510 invokeLater(new Runnable() {
513 model.insertNodeInto(tmpNode, parent, 0);
517 DaemonThreadFactory.execute(new Runnable() {
521 final List<InfoNode> children = new ArrayList<InfoNode>(parent.createChildren(dbmeta));
522 invokeLater(new Runnable() {
525 for (InfoNode child : children) {
526 model.insertNodeInto(child, parent, parent.getChildCount());
528 model.removeNodeFromParent(tmpNode);
531 } catch (SQLException ex) {
532 throw new RuntimeException(ex);
542 for (TreeWillExpandListener listener : getListeners(TreeWillExpandListener.class).clone()) {
543 removeTreeWillExpandListener(listener);
545 setModel(new DefaultTreeModel(null));
546 currentConnector = null;
548 if (log.isDebugEnabled()) {
549 log.debug("cleared");
553 static String generateEquivalentJoinClause(List<ColumnNode> nodes) {
554 if (nodes.isEmpty()) {
557 ListMap tm = new ListMap();
558 ListMap cm = new ListMap();
559 for (ColumnNode node : nodes) {
560 final String tableName = node.getTableNode().getName();
561 final String columnName = node.getName();
562 tm.add(tableName, columnName);
563 cm.add(columnName, String.format("%s.%s", tableName, columnName));
565 List<String> expressions = new ArrayList<String>();
566 if (tm.size() == 1) {
567 for (ColumnNode node : nodes) {
568 expressions.add(String.format("%s=?", node.getName()));
571 final String tableName = nodes.get(0).getTableNode().getName();
572 for (String c : tm.get(tableName)) {
573 expressions.add(String.format("%s.%s=?", tableName, c));
575 for (Entry<String, List<String>> entry : cm.entrySet()) {
576 if (!entry.getKey().equals(tableName) && entry.getValue().size() == 1) {
577 expressions.add(String.format("%s=?", entry.getValue().get(0)));
580 for (Entry<String, List<String>> entry : cm.entrySet()) {
581 Object[] a = entry.getValue().toArray();
582 final int n = a.length;
583 for (int i = 0; i < n; i++) {
584 for (int j = i + 1; j < n; j++) {
585 expressions.add(String.format("%s=%s", a[i], a[j]));
590 return TextUtilities.join(" AND ", expressions) + ';';
594 public TreePath[] getSelectionPaths() {
595 TreePath[] a = super.getSelectionPaths();
597 return new TreePath[0];
604 private static final class ListMap extends LinkedHashMap<String, List<String>> {
610 void add(String key, String value) {
611 if (get(key) == null) {
612 put(key, new ArrayList<String>());
619 private static class Renderer extends DefaultTreeCellRenderer {
626 public Component getTreeCellRendererComponent(JTree tree,
633 super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
634 if (value instanceof InfoNode) {
635 setIcon(Utilities.getImageIcon(((InfoNode)value).getIconName()));
637 if (value instanceof ColumnNode) {
638 if (showColumnNumber) {
639 TreePath path = tree.getPathForRow(row);
641 TreePath parent = path.getParentPath();
642 if (parent != null) {
643 final int index = row - tree.getRowForPath(parent);
644 setText(String.format("%d %s", index, getText()));
654 private abstract static class InfoNode extends DefaultMutableTreeNode {
656 InfoNode(Object userObject) {
657 super(userObject, true);
661 public boolean isLeaf() {
665 abstract protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException;
667 String getIconName() {
668 final String className = getClass().getName();
669 final String nodeType = className.replaceFirst(".+?([^\\$]+)Node$", "$1");
670 return "node-" + nodeType.toLowerCase() + ".png";
673 protected String getNodeFullName() {
674 return String.valueOf(userObject);
677 static List<TableTypeNode> getTableTypeNodes(DatabaseMetaData dbmeta,
679 String schema) throws SQLException {
680 List<TableTypeNode> a = new ArrayList<TableTypeNode>();
681 ResultSet rs = dbmeta.getTableTypes();
684 TableTypeNode typeNode = new TableTypeNode(catalog, schema, rs.getString(1));
685 if (typeNode.hasItems(dbmeta)) {
693 a.add(new TableTypeNode(catalog, schema, "TABLE"));
700 private static class ConnectorNode extends InfoNode {
702 ConnectorNode(String name) {
707 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
708 List<InfoNode> a = new ArrayList<InfoNode>();
709 if (dbmeta.supportsCatalogsInDataManipulation()) {
710 ResultSet rs = dbmeta.getCatalogs();
713 a.add(new CatalogNode(rs.getString(1)));
718 } else if (dbmeta.supportsSchemasInDataManipulation()) {
719 ResultSet rs = dbmeta.getSchemas();
722 a.add(new SchemaNode(null, rs.getString(1)));
728 a.addAll(getTableTypeNodes(dbmeta, null, null));
735 private static final class CatalogNode extends InfoNode {
737 private final String name;
739 CatalogNode(String name) {
745 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
746 List<InfoNode> a = new ArrayList<InfoNode>();
747 if (dbmeta.supportsSchemasInDataManipulation()) {
748 ResultSet rs = dbmeta.getSchemas();
751 a.add(new SchemaNode(name, rs.getString(1)));
757 a.addAll(getTableTypeNodes(dbmeta, name, null));
764 private static final class SchemaNode extends InfoNode {
766 private final String catalog;
767 private final String schema;
769 SchemaNode(String catalog, String schema) {
771 this.catalog = catalog;
772 this.schema = schema;
776 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
777 List<InfoNode> a = new ArrayList<InfoNode>();
778 a.addAll(getTableTypeNodes(dbmeta, catalog, schema));
784 private static final class TableTypeNode extends InfoNode {
786 private static final String ICON_NAME_FORMAT = "node-tabletype-%s.png";
788 private final String catalog;
789 private final String schema;
790 private final String tableType;
792 TableTypeNode(String catalog, String schema, String tableType) {
794 this.catalog = catalog;
795 this.schema = schema;
796 this.tableType = tableType;
800 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
801 List<InfoNode> a = new ArrayList<InfoNode>();
802 ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
805 final String table = rs.getString(3);
806 final String type = rs.getString(4);
807 final boolean kindOfTable = type.matches("TABLE|VIEW|SYNONYM");
808 a.add(new TableNode(catalog, schema, table, kindOfTable));
817 String getIconName() {
818 final String name = String.format(ICON_NAME_FORMAT, getUserObject());
819 if (getClass().getResource("icon/" + name) == null) {
820 return String.format(ICON_NAME_FORMAT, "");
825 boolean hasItems(DatabaseMetaData dbmeta) throws SQLException {
826 ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
836 static final class TableNode extends InfoNode {
838 private final String catalog;
839 private final String schema;
840 private final String name;
841 private final boolean kindOfTable;
843 TableNode(String catalog, String schema, String name, boolean kindOfTable) {
845 this.catalog = catalog;
846 this.schema = schema;
848 this.kindOfTable = kindOfTable;
852 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
853 List<InfoNode> a = new ArrayList<InfoNode>();
854 ResultSet rs = dbmeta.getColumns(catalog, schema, name, null);
857 a.add(new ColumnNode(rs.getString(4),
870 public boolean isLeaf() {
875 protected String getNodeFullName() {
876 List<String> a = new ArrayList<String>();
877 if (catalog != null) {
880 if (schema != null) {
884 return TextUtilities.join(".", a);
891 boolean isKindOfTable() {
897 static final class ColumnNode extends InfoNode {
899 private final String name;
900 private final TableNode tableNode;
902 ColumnNode(String name, String type, int size, String nulls, TableNode tableNode) {
903 super(format(name, type, size, nulls));
904 setAllowsChildren(false);
906 this.tableNode = tableNode;
913 TableNode getTableNode() {
917 private static String format(String name, String type, int size, String nulls) {
918 final String nonNull = "NO".equals(nulls) ? " NOT NULL" : "";
919 return String.format("%s [%s(%d)%s]", name, type, size, nonNull);
923 public boolean isLeaf() {
928 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
933 protected String getNodeFullName() {
934 return String.format("%s.%s", tableNode.getNodeFullName(), name);