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 generateWherePhrase();
92 } else if (ev.isAnyOf(generateSelectPhrase)) {
93 generateSelectPhrase();
94 } else if (ev.isAnyOf(generateUpdateStatement)) {
95 generateUpdateStatement();
96 } else if (ev.isAnyOf(generateInsertStatement)) {
97 generateInsertStatement();
98 } else if (ev.isAnyOf(jumpToColumnByName)) {
100 } else if (ev.isAnyOf(toggleShowColumnNumber)) {
101 showColumnNumber = !showColumnNumber;
104 log.warn("not expected: Event=%s", ev);
106 log.atExit("anyActionPerformed");
109 private void insertTextToTextArea(String s) {
110 AnyActionEvent ev = new AnyActionEvent(this, ConsoleTextArea.ActionKey.insertText, s);
111 anyActionListener.anyActionPerformed(ev);
114 private void copySimpleName() {
115 TreePath[] paths = getSelectionPaths();
116 if (paths == null || paths.length == 0) {
119 List<String> names = new ArrayList<String>(paths.length);
120 for (TreePath path : paths) {
124 Object o = path.getLastPathComponent();
125 assert o instanceof InfoNode;
127 if (o instanceof ColumnNode) {
128 name = ((ColumnNode)o).getName();
129 } else if (o instanceof TableNode) {
130 name = ((TableNode)o).getName();
136 ClipboardHelper.setStrings(names);
139 private void copyFullName() {
140 TreePath[] paths = getSelectionPaths();
141 if (paths == null || paths.length == 0) {
144 List<String> names = new ArrayList<String>(paths.length);
145 for (TreePath path : paths) {
149 Object o = path.getLastPathComponent();
150 assert o instanceof InfoNode;
151 names.add(((InfoNode)o).getNodeFullName());
153 ClipboardHelper.setStrings(names);
156 private void generateWherePhrase() {
157 List<ColumnNode> columns = collectColumnNode(getSelectionPaths());
158 if (!columns.isEmpty()) {
159 insertTextToTextArea(generateEquivalentJoinClause(columns));
163 private void generateSelectPhrase() {
164 TreePath[] paths = getSelectionPaths();
165 List<ColumnNode> columns = collectColumnNode(paths);
166 final String columnString;
167 final String tableString;
168 if (columns.isEmpty()) {
169 List<TableNode> tableNodes = collectTableNode(paths);
170 if (tableNodes.isEmpty()) {
174 tableString = join(",", tableNodes);
176 List<String> columnNames = new ArrayList<String>();
177 String[] tableNames = collectTableName(columns);
178 final boolean one = tableNames.length == 1;
179 for (ColumnNode node : columns) {
180 columnNames.add(one ? node.getName() : node.getNodeFullName());
182 columnString = join(", ", columnNames);
183 tableString = join(",", Arrays.asList(tableNames));
185 insertTextToTextArea(String.format("SELECT %s FROM %s WHERE ", columnString, tableString));
188 private void generateUpdateStatement() {
189 TreePath[] paths = getSelectionPaths();
190 List<ColumnNode> columns = collectColumnNode(paths);
191 String[] tableNames = collectTableName(columns);
192 if (tableNames.length == 0) {
195 if (tableNames.length != 1 || columns.isEmpty()) {
196 showInformationMessageDialog(this, res.get("e.enables-select-just-1-table"), "");
199 List<String> columnExpressions = new ArrayList<String>();
200 for (ColumnNode columnNode : columns) {
201 columnExpressions.add(columnNode.getName() + "=?");
203 insertTextToTextArea(String.format("UPDATE %s SET %s WHERE ",
205 join(",", columnExpressions)));
208 private void generateInsertStatement() {
209 TreePath[] paths = getSelectionPaths();
210 List<ColumnNode> columns = collectColumnNode(paths);
211 final String tableName;
212 final int tableCount;
213 if (columns.isEmpty()) {
214 List<TableNode> tables = collectTableNode(paths);
215 if (tables.isEmpty()) {
218 TableNode tableNode = tables.get(0);
219 if (tableNode.getChildCount() == 0) {
220 showInformationMessageDialog(this,
221 res.get("i.can-only-use-after-tablenode-expanded"),
225 @SuppressWarnings("unchecked")
226 List<ColumnNode> list = Collections.list(tableNode.children());
227 columns.addAll(list);
228 tableName = tableNode.getNodeFullName();
229 tableCount = tables.size();
231 String[] tableNames = collectTableName(columns);
232 tableCount = tableNames.length;
233 if (tableCount == 0) {
236 tableName = tableNames[0];
238 if (tableCount != 1) {
239 showInformationMessageDialog(this, res.get("e.enables-select-just-1-table"), "");
242 List<String> columnNames = new ArrayList<String>();
243 for (ColumnNode node : columns) {
244 columnNames.add(node.getName());
246 final int columnCount = columnNames.size();
247 insertTextToTextArea(String.format("INSERT INTO %s (%s) VALUES (%s);%s",
249 join(",", columnNames),
250 join(",", nCopies(columnCount, "?")),
251 join(",", nCopies(columnCount, ""))));
254 private void jumpToColumnByName() {
255 TreePath[] paths = getSelectionPaths();
256 if (paths == null || paths.length == 0) {
259 final TreePath path = paths[0];
260 Object o = path.getLastPathComponent();
261 if (o instanceof ColumnNode) {
262 ColumnNode node = (ColumnNode)o;
263 AnyActionEvent ev = new AnyActionEvent(this,
264 ResultSetTable.ActionKey.jumpToColumn,
266 anyActionListener.anyActionPerformed(ev);
270 private static List<ColumnNode> collectColumnNode(TreePath[] paths) {
271 List<ColumnNode> a = new ArrayList<ColumnNode>();
273 for (TreePath path : paths) {
274 InfoNode node = (InfoNode)path.getLastPathComponent();
275 if (node instanceof ColumnNode) {
276 ColumnNode columnNode = (ColumnNode)node;
284 private static List<TableNode> collectTableNode(TreePath[] paths) {
285 List<TableNode> a = new ArrayList<TableNode>();
287 for (TreePath path : paths) {
288 InfoNode node = (InfoNode)path.getLastPathComponent();
289 if (node instanceof TableNode) {
290 TableNode columnNode = (TableNode)node;
298 private static String[] collectTableName(List<ColumnNode> columnNodes) {
299 Set<String> tableNames = new LinkedHashSet<String>();
300 for (final ColumnNode node : columnNodes) {
301 tableNames.add(node.getTableNode().getNodeFullName());
303 return tableNames.toArray(new String[tableNames.size()]);
306 static String generateEquivalentJoinClause(List<ColumnNode> nodes) {
307 if (nodes.isEmpty()) {
310 ListMap tm = new ListMap();
311 ListMap cm = new ListMap();
312 for (ColumnNode node : nodes) {
313 final String tableName = node.getTableNode().getName();
314 final String columnName = node.getName();
315 tm.add(tableName, columnName);
316 cm.add(columnName, String.format("%s.%s", tableName, columnName));
318 List<String> expressions = new ArrayList<String>();
319 if (tm.size() == 1) {
320 for (ColumnNode node : nodes) {
321 expressions.add(String.format("%s=?", node.getName()));
324 final String tableName = nodes.get(0).getTableNode().getName();
325 for (String c : tm.get(tableName)) {
326 expressions.add(String.format("%s.%s=?", tableName, c));
328 for (Entry<String, List<String>> entry : cm.entrySet()) {
329 if (!entry.getKey().equals(tableName) && entry.getValue().size() == 1) {
330 expressions.add(String.format("%s=?", entry.getValue().get(0)));
333 for (Entry<String, List<String>> entry : cm.entrySet()) {
334 Object[] a = entry.getValue().toArray();
335 final int n = a.length;
336 for (int i = 0; i < n; i++) {
337 for (int j = i + 1; j < n; j++) {
338 expressions.add(String.format("%s=%s", a[i], a[j]));
343 return join(" AND ", expressions) + ';';
349 public boolean search(Matcher matcher) {
350 return search(resolveTargetPath(getSelectionPath()), matcher);
353 private static TreePath resolveTargetPath(TreePath path) {
355 TreePath parent = path.getParentPath();
356 if (parent != null) {
363 private boolean search(TreePath path, Matcher matcher) {
367 TreeNode node = (TreeNode)path.getLastPathComponent();
371 boolean found = false;
372 found = matcher.find(node.toString());
374 addSelectionPath(path);
376 removeSelectionPath(path);
378 if (!node.isLeaf() && node.getChildCount() >= 0) {
379 @SuppressWarnings("unchecked")
380 Iterable<DefaultMutableTreeNode> children = Collections.list(node.children());
381 for (DefaultMutableTreeNode child : children) {
382 if (search(path.pathByAddingChild(child), matcher)) {
391 public void reset() {
398 * Refreshes the root and its children.
399 * @param env Environment
400 * @throws SQLException
402 void refreshRoot(Environment env) throws SQLException {
403 Connector c = env.getCurrentConnector();
405 if (log.isDebugEnabled()) {
406 log.debug("not connected");
408 currentConnector = null;
411 if (c == currentConnector && getModel().getRoot() != null) {
412 if (log.isDebugEnabled()) {
413 log.debug("not changed");
417 if (log.isDebugEnabled()) {
418 log.debug("updating");
420 // initializing models
421 ConnectorNode connectorNode = new ConnectorNode(c.getName());
422 DefaultTreeModel model = new DefaultTreeModel(connectorNode);
424 final DefaultTreeSelectionModel m = new DefaultTreeSelectionModel();
425 m.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
426 setSelectionModel(m);
427 // initializing nodes
428 final DatabaseMetaData dbmeta = env.getCurrentConnection().getMetaData();
429 final Set<InfoNode> createdStatusSet = new HashSet<InfoNode>();
430 expandNode(connectorNode, dbmeta);
431 createdStatusSet.add(connectorNode);
433 addTreeWillExpandListener(new TreeWillExpandListener() {
435 public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
436 TreePath path = event.getPath();
437 final Object lastPathComponent = path.getLastPathComponent();
438 if (!createdStatusSet.contains(lastPathComponent)) {
439 InfoNode node = (InfoNode)lastPathComponent;
443 createdStatusSet.add(node);
445 expandNode(node, dbmeta);
446 } catch (SQLException ex) {
447 throw new RuntimeException(ex);
452 public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
456 this.dbmeta = dbmeta;
459 setRootVisible(true);
460 this.currentConnector = c;
463 File confFile = new File(Bootstrap.getDirectory(), "autoexpansion.tsv");
464 if (confFile.exists() && confFile.length() > 0) {
465 AnyAction aa = new AnyAction(this);
466 Scanner r = new Scanner(confFile);
468 while (r.hasNextLine()) {
469 final String line = r.nextLine();
470 if (line.matches("^\\s*#.*")) {
473 aa.doParallel("expandNodes", Arrays.asList(line.split("\t")));
479 } catch (IOException ex) {
480 throw new IllegalStateException(ex);
484 void expandNodes(List<String> a) {
485 long startTime = System.currentTimeMillis();
486 AnyAction aa = new AnyAction(this);
488 while (index < a.size()) {
489 final String s = a.subList(0, index + 1).toString();
490 for (int i = 0, n = getRowCount(); i < n; i++) {
493 target = getPathForRow(i);
494 } catch (IndexOutOfBoundsException ex) {
495 // FIXME when IndexOutOfBoundsException was thrown at expandNodes
499 if (target != null && target.toString().equals(s)) {
500 if (!isExpanded(target)) {
501 aa.doLater("expandLater", target);
502 Utilities.sleep(200L);
508 if (System.currentTimeMillis() - startTime > 5000L) {
514 // called by expandNodes
515 @SuppressWarnings("unused")
516 private void expandLater(TreePath parent) {
521 * Refreshes a node and its children.
524 void refresh(InfoNode node) {
525 if (dbmeta == null) {
528 node.removeAllChildren();
529 final DefaultTreeModel model = (DefaultTreeModel)getModel();
532 expandNode(node, dbmeta);
533 } catch (SQLException ex) {
534 throw new RuntimeException(ex);
542 * @throws SQLException
544 void expandNode(final InfoNode parent, final DatabaseMetaData dbmeta) throws SQLException {
545 if (parent.isLeaf()) {
548 final DefaultTreeModel model = (DefaultTreeModel)getModel();
549 final InfoNode tmpNode = new InfoNode(res.get("i.paren-in-processing")) {
551 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
552 return Collections.emptyList();
555 public boolean isLeaf() {
559 invokeLater(new Runnable() {
562 model.insertNodeInto(tmpNode, parent, 0);
566 DaemonThreadFactory.execute(new Runnable() {
570 final List<InfoNode> children = new ArrayList<InfoNode>(parent.createChildren(dbmeta));
571 invokeLater(new Runnable() {
574 for (InfoNode child : children) {
575 model.insertNodeInto(child, parent, parent.getChildCount());
577 model.removeNodeFromParent(tmpNode);
580 } catch (SQLException ex) {
581 throw new RuntimeException(ex);
591 for (TreeWillExpandListener listener : getListeners(TreeWillExpandListener.class).clone()) {
592 removeTreeWillExpandListener(listener);
594 setModel(new DefaultTreeModel(null));
595 currentConnector = null;
597 if (log.isDebugEnabled()) {
598 log.debug("cleared");
603 public TreePath[] getSelectionPaths() {
604 TreePath[] a = super.getSelectionPaths();
606 return new TreePath[0];
613 private static final class ListMap extends LinkedHashMap<String, List<String>> {
619 void add(String key, String value) {
620 if (get(key) == null) {
621 put(key, new ArrayList<String>());
628 private static class Renderer extends DefaultTreeCellRenderer {
635 public Component getTreeCellRendererComponent(JTree tree,
642 super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
643 if (value instanceof InfoNode) {
644 setIcon(Utilities.getImageIcon(((InfoNode)value).getIconName()));
646 if (value instanceof ColumnNode) {
647 if (showColumnNumber) {
648 TreePath path = tree.getPathForRow(row);
650 TreePath parent = path.getParentPath();
651 if (parent != null) {
652 final int index = row - tree.getRowForPath(parent);
653 setText(String.format("%d %s", index, getText()));
663 private abstract static class InfoNode extends DefaultMutableTreeNode {
665 InfoNode(Object userObject) {
666 super(userObject, true);
670 public boolean isLeaf() {
674 abstract protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException;
676 String getIconName() {
677 final String className = getClass().getName();
678 final String nodeType = className.replaceFirst(".+?([^\\$]+)Node$", "$1");
679 return "node-" + nodeType.toLowerCase() + ".png";
682 protected String getNodeFullName() {
683 return String.valueOf(userObject);
686 static List<TableTypeNode> getTableTypeNodes(DatabaseMetaData dbmeta,
688 String schema) throws SQLException {
689 List<TableTypeNode> a = new ArrayList<TableTypeNode>();
690 ResultSet rs = dbmeta.getTableTypes();
693 TableTypeNode typeNode = new TableTypeNode(catalog, schema, rs.getString(1));
694 if (typeNode.hasItems(dbmeta)) {
702 a.add(new TableTypeNode(catalog, schema, "TABLE"));
709 private static class ConnectorNode extends InfoNode {
711 ConnectorNode(String name) {
716 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
717 List<InfoNode> a = new ArrayList<InfoNode>();
718 if (dbmeta.supportsCatalogsInDataManipulation()) {
719 ResultSet rs = dbmeta.getCatalogs();
722 a.add(new CatalogNode(rs.getString(1)));
727 } else if (dbmeta.supportsSchemasInDataManipulation()) {
728 ResultSet rs = dbmeta.getSchemas();
731 a.add(new SchemaNode(null, rs.getString(1)));
737 a.addAll(getTableTypeNodes(dbmeta, null, null));
744 private static final class CatalogNode extends InfoNode {
746 private final String name;
748 CatalogNode(String name) {
754 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
755 List<InfoNode> a = new ArrayList<InfoNode>();
756 if (dbmeta.supportsSchemasInDataManipulation()) {
757 ResultSet rs = dbmeta.getSchemas();
760 a.add(new SchemaNode(name, rs.getString(1)));
766 a.addAll(getTableTypeNodes(dbmeta, name, null));
773 private static final class SchemaNode extends InfoNode {
775 private final String catalog;
776 private final String schema;
778 SchemaNode(String catalog, String schema) {
780 this.catalog = catalog;
781 this.schema = schema;
785 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
786 List<InfoNode> a = new ArrayList<InfoNode>();
787 a.addAll(getTableTypeNodes(dbmeta, catalog, schema));
793 private static final class TableTypeNode extends InfoNode {
795 private static final String ICON_NAME_FORMAT = "node-tabletype-%s.png";
797 private final String catalog;
798 private final String schema;
799 private final String tableType;
801 TableTypeNode(String catalog, String schema, String tableType) {
803 this.catalog = catalog;
804 this.schema = schema;
805 this.tableType = tableType;
809 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
810 List<InfoNode> a = new ArrayList<InfoNode>();
811 ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
814 final String table = rs.getString(3);
815 final String type = rs.getString(4);
816 final boolean kindOfTable = type.matches("TABLE|VIEW|SYNONYM");
817 a.add(new TableNode(catalog, schema, table, kindOfTable));
826 String getIconName() {
827 final String name = String.format(ICON_NAME_FORMAT, getUserObject());
828 if (getClass().getResource("icon/" + name) == null) {
829 return String.format(ICON_NAME_FORMAT, "");
834 boolean hasItems(DatabaseMetaData dbmeta) throws SQLException {
835 ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
845 static final class TableNode extends InfoNode {
847 private final String catalog;
848 private final String schema;
849 private final String name;
850 private final boolean kindOfTable;
852 TableNode(String catalog, String schema, String name, boolean kindOfTable) {
854 this.catalog = catalog;
855 this.schema = schema;
857 this.kindOfTable = kindOfTable;
861 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
862 List<InfoNode> a = new ArrayList<InfoNode>();
863 ResultSet rs = dbmeta.getColumns(catalog, schema, name, null);
866 a.add(new ColumnNode(rs.getString(4),
879 public boolean isLeaf() {
884 protected String getNodeFullName() {
885 List<String> a = new ArrayList<String>();
886 if (catalog != null) {
889 if (schema != null) {
900 boolean isKindOfTable() {
906 static final class ColumnNode extends InfoNode {
908 private final String name;
909 private final TableNode tableNode;
911 ColumnNode(String name, String type, int size, String nulls, TableNode tableNode) {
912 super(format(name, type, size, nulls));
913 setAllowsChildren(false);
915 this.tableNode = tableNode;
922 TableNode getTableNode() {
926 private static String format(String name, String type, int size, String nulls) {
927 final String nonNull = "NO".equals(nulls) ? " NOT NULL" : "";
928 return String.format("%s [%s(%d)%s]", name, type, size, nonNull);
932 public boolean isLeaf() {
937 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
942 protected String getNodeFullName() {
943 return String.format("%s.%s", tableNode.getNodeFullName(), name);