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 static final Logger log = Logger.getLogger(DatabaseInfoTree.class);
46 private static final ResourceManager res = ResourceManager.getInstance(DatabaseInfoTree.class);
48 private static final String TABLE_TYPE_TABLE = "TABLE";
49 private static final String TABLE_TYPE_VIEW = "VIEW";
50 private static final String TABLE_TYPE_INDEX = "INDEX";
51 private static final String TABLE_TYPE_SEQUENCE = "SEQUENCE";
52 static final List<String> DEFAULT_TABLE_TYPES = //
53 Arrays.asList(TABLE_TYPE_TABLE, TABLE_TYPE_VIEW, TABLE_TYPE_INDEX, TABLE_TYPE_SEQUENCE);
55 static volatile boolean showColumnNumber;
57 private Connector currentConnector;
58 private DatabaseMetaData dbmeta;
59 private AnyActionListener anyActionListener;
61 DatabaseInfoTree(AnyActionListener anyActionListener) {
62 this.anyActionListener = anyActionListener;
63 setRootVisible(false);
64 setShowsRootHandles(false);
65 setScrollsOnExpand(true);
66 setCellRenderer(new Renderer());
67 setModel(new DefaultTreeModel(null));
69 int sckm = Utilities.getMenuShortcutKeyMask();
70 AnyAction aa = new AnyAction(this);
71 aa.bindKeyStroke(false, copy, KeyStroke.getKeyStroke(VK_C, sckm));
72 aa.bindSelf(copySimpleName, getKeyStroke(VK_C, sckm | ALT_DOWN_MASK));
73 aa.bindSelf(copyFullName, getKeyStroke(VK_C, sckm | SHIFT_DOWN_MASK));
77 protected void processMouseEvent(MouseEvent e) {
78 super.processMouseEvent(e);
79 if (e.getID() == MouseEvent.MOUSE_CLICKED && e.getClickCount() % 2 == 0) {
80 anyActionPerformed(new AnyActionEvent(this, jumpToColumnByName));
85 public void anyActionPerformed(AnyActionEvent ev) {
86 log.atEnter("anyActionPerformed", ev);
87 if (ev.isAnyOf(copy)) {
88 final String cmd = ev.getActionCommand();
89 Action action = getActionMap().get(cmd);
91 action.actionPerformed(new ActionEvent(this, 1001, cmd));
93 } else if (ev.isAnyOf(copySimpleName)) {
95 } else if (ev.isAnyOf(copyFullName)) {
97 } else if (ev.isAnyOf(refresh)) {
98 for (TreePath path : getSelectionPaths()) {
99 refresh((InfoNode)path.getLastPathComponent());
101 } else if (ev.isAnyOf(generateWherePhrase)) {
102 List<ColumnNode> columnNodes = new ArrayList<ColumnNode>();
103 for (TreeNode node : getSelectionNodes()) {
104 if (node instanceof ColumnNode) {
105 columnNodes.add((ColumnNode)node);
108 final String phrase = generateEquivalentJoinClause(columnNodes);
109 if (phrase.length() > 0) {
110 insertTextIntoTextArea(addCommas(phrase));
112 } else if (ev.isAnyOf(generateSelectPhrase)) {
113 final String phrase = generateSelectPhrase(getSelectionNodes());
114 if (phrase.length() > 0) {
115 insertTextIntoTextArea(phrase + " WHERE ");
117 } else if (ev.isAnyOf(generateUpdateStatement, generateInsertStatement)) {
118 final boolean isInsert = ev.isAnyOf(generateInsertStatement);
120 final String phrase = generateUpdateOrInsertPhrase(getSelectionNodes(), isInsert);
121 if (phrase.length() > 0) {
123 insertTextIntoTextArea(addCommas(phrase));
125 insertTextIntoTextArea(phrase + " WHERE ");
128 } catch (IllegalArgumentException ex) {
129 showInformationMessageDialog(this, ex.getMessage(), "");
131 } else if (ev.isAnyOf(jumpToColumnByName)) {
132 jumpToColumnByName();
133 } else if (ev.isAnyOf(toggleShowColumnNumber)) {
134 showColumnNumber = !showColumnNumber;
137 log.warn("not expected: Event=%s", ev);
139 log.atExit("anyActionPerformed");
143 public TreePath[] getSelectionPaths() {
144 TreePath[] a = super.getSelectionPaths();
146 return new TreePath[0];
151 List<TreeNode> getSelectionNodes() {
152 List<TreeNode> a = new ArrayList<TreeNode>();
153 for (TreePath path : getSelectionPaths()) {
154 a.add((TreeNode)path.getLastPathComponent());
159 private void insertTextIntoTextArea(String s) {
160 AnyActionEvent ev = new AnyActionEvent(this, ConsoleTextArea.ActionKey.insertText, s);
161 anyActionListener.anyActionPerformed(ev);
164 private void copySimpleName() {
165 TreePath[] paths = getSelectionPaths();
166 if (paths == null || paths.length == 0) {
169 List<String> names = new ArrayList<String>(paths.length);
170 for (TreePath path : paths) {
174 Object o = path.getLastPathComponent();
175 assert o instanceof InfoNode;
177 if (o instanceof ColumnNode) {
178 name = ((ColumnNode)o).getName();
179 } else if (o instanceof TableNode) {
180 name = ((TableNode)o).getName();
186 ClipboardHelper.setStrings(names);
189 private void copyFullName() {
190 TreePath[] paths = getSelectionPaths();
191 if (paths == null || paths.length == 0) {
194 List<String> names = new ArrayList<String>(paths.length);
195 for (TreePath path : paths) {
199 Object o = path.getLastPathComponent();
200 assert o instanceof InfoNode;
201 names.add(((InfoNode)o).getNodeFullName());
203 ClipboardHelper.setStrings(names);
206 private void jumpToColumnByName() {
207 TreePath[] paths = getSelectionPaths();
208 if (paths == null || paths.length == 0) {
211 final TreePath path = paths[0];
212 Object o = path.getLastPathComponent();
213 if (o instanceof ColumnNode) {
214 ColumnNode node = (ColumnNode)o;
215 AnyActionEvent ev = new AnyActionEvent(this,
216 ResultSetTable.ActionKey.jumpToColumn,
218 anyActionListener.anyActionPerformed(ev);
222 private static String addCommas(String phrase) {
224 for (final char ch : phrase.toCharArray()) {
230 return String.format("%s;%s", phrase, join("", nCopies(c - 1, ",")));
235 static String generateEquivalentJoinClause(List<ColumnNode> nodes) {
236 if (nodes.isEmpty()) {
239 Set<String> tableNames = new LinkedHashSet<String>();
240 ListMap columnMap = new ListMap();
241 for (ColumnNode node : nodes) {
242 final String tableName = node.getTableNode().getNodeFullName();
243 final String columnName = node.getName();
244 tableNames.add(tableName);
245 columnMap.add(columnName, String.format("%s.%s", tableName, columnName));
247 assert tableNames.size() >= 1;
248 List<String> expressions = new ArrayList<String>();
249 if (tableNames.size() == 1) {
250 for (ColumnNode node : nodes) {
251 expressions.add(String.format("%s=?", node.getName()));
253 } else { // size >= 2
254 List<String> expressions2 = new ArrayList<String>();
255 for (Entry<String, List<String>> entry : columnMap.entrySet()) {
256 List<String> a = entry.getValue();
257 final int n = a.size();
259 expressions2.add(String.format("%s=?", a.get(0)));
261 for (int i = 0; i < n; i++) {
262 for (int j = i + 1; j < n; j++) {
263 expressions.add(String.format("%s=%s", a.get(i), a.get(j)));
268 expressions.addAll(expressions2);
270 return String.format("%s", join(" AND ", expressions));
273 static String generateSelectPhrase(List<TreeNode> nodes) {
274 Set<String> tableNames = new LinkedHashSet<String>();
275 ListMap columnMap = new ListMap();
276 for (TreeNode node : nodes) {
277 if (node instanceof TableNode) {
278 final String tableFullName = ((TableNode)node).getNodeFullName();
279 tableNames.add(tableFullName);
280 columnMap.add(tableFullName);
281 } else if (node instanceof ColumnNode) {
282 ColumnNode cn = (ColumnNode)node;
283 final String tableFullName = cn.getTableNode().getNodeFullName();
284 tableNames.add(tableFullName);
285 columnMap.add(tableFullName, cn.getNodeFullName());
288 if (tableNames.isEmpty()) {
291 List<String> columnNames = new ArrayList<String>();
292 if (tableNames.size() == 1) {
293 List<String> a = new ArrayList<String>();
294 for (TreeNode node : nodes) {
295 if (node instanceof ColumnNode) {
296 ColumnNode cn = (ColumnNode)node;
301 columnNames.add("*");
303 columnNames.addAll(a);
305 } else { // size >= 2
306 for (Entry<String, List<String>> entry : columnMap.entrySet()) {
307 final List<String> columnsInTable = entry.getValue();
308 if (columnsInTable.isEmpty()) {
309 columnNames.add(entry.getKey() + ".*");
311 columnNames.addAll(columnsInTable);
315 return String.format("SELECT %s FROM %s", join(", ", columnNames), join(", ", tableNames));
318 static String generateUpdateOrInsertPhrase(List<TreeNode> nodes, boolean isInsert) {
319 Set<String> tableNames = new LinkedHashSet<String>();
320 ListMap columnMap = new ListMap();
321 for (TreeNode node : nodes) {
322 if (node instanceof TableNode) {
323 final String tableFullName = ((TableNode)node).getNodeFullName();
324 tableNames.add(tableFullName);
325 columnMap.add(tableFullName);
326 } else if (node instanceof ColumnNode) {
327 ColumnNode cn = (ColumnNode)node;
328 final String tableFullName = cn.getTableNode().getNodeFullName();
329 tableNames.add(tableFullName);
330 columnMap.add(tableFullName, cn.getName());
333 if (tableNames.isEmpty()) {
336 if (tableNames.size() >= 2) {
337 throw new IllegalArgumentException(res.get("e.enables-select-just-1-table"));
339 final String tableName = join("", tableNames);
340 List<String> columnsInTable = columnMap.get(tableName);
341 if (columnsInTable.isEmpty()) {
343 List<TableNode> tableNodes = new ArrayList<TableNode>();
344 for (TreeNode node : nodes) {
345 if (node instanceof TableNode) {
346 tableNodes.add((TableNode)node);
350 TableNode tableNode = tableNodes.get(0);
351 if (tableNode.getChildCount() == 0) {
352 throw new IllegalArgumentException(res.get("i.can-only-use-after-tablenode-expanded"));
354 for (int i = 0, n = tableNode.getChildCount(); i < n; i++) {
355 ColumnNode child = (ColumnNode)tableNode.getChildAt(i);
356 columnsInTable.add(child.getName());
364 final int columnCount = columnsInTable.size();
365 phrase = String.format("INSERT INTO %s (%s) VALUES (%s)",
367 join(",", columnsInTable),
368 join(",", nCopies(columnCount, "?")));
370 List<String> columnExpressions = new ArrayList<String>();
371 for (final String columnName : columnsInTable) {
372 columnExpressions.add(columnName + "=?");
374 phrase = String.format("UPDATE %s SET %s", tableName, join(", ", columnExpressions));
382 public boolean search(Matcher matcher) {
383 return search(resolveTargetPath(getSelectionPath()), matcher);
386 private static TreePath resolveTargetPath(TreePath path) {
388 TreePath parent = path.getParentPath();
389 if (parent != null) {
396 private boolean search(TreePath path, Matcher matcher) {
400 TreeNode node = (TreeNode)path.getLastPathComponent();
404 boolean found = false;
405 found = matcher.find(node.toString());
407 addSelectionPath(path);
409 removeSelectionPath(path);
411 if (!node.isLeaf() && node.getChildCount() >= 0) {
412 @SuppressWarnings("unchecked")
413 Iterable<DefaultMutableTreeNode> children = Collections.list(node.children());
414 for (DefaultMutableTreeNode child : children) {
415 if (search(path.pathByAddingChild(child), matcher)) {
424 public void reset() {
431 * Refreshes the root and its children.
432 * @param env Environment
433 * @throws SQLException
435 void refreshRoot(Environment env) throws SQLException {
436 Connector c = env.getCurrentConnector();
438 if (log.isDebugEnabled()) {
439 log.debug("not connected");
441 currentConnector = null;
444 if (c == currentConnector && getModel().getRoot() != null) {
445 if (log.isDebugEnabled()) {
446 log.debug("not changed");
450 if (log.isDebugEnabled()) {
451 log.debug("updating");
453 // initializing models
454 ConnectorNode connectorNode = new ConnectorNode(c.getName());
455 DefaultTreeModel model = new DefaultTreeModel(connectorNode);
457 final DefaultTreeSelectionModel m = new DefaultTreeSelectionModel();
458 m.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
459 setSelectionModel(m);
460 // initializing nodes
461 final DatabaseMetaData dbmeta = env.getCurrentConnection().getMetaData();
462 final Set<InfoNode> createdStatusSet = new HashSet<InfoNode>();
463 expandNode(connectorNode, dbmeta);
464 createdStatusSet.add(connectorNode);
466 class TreeWillExpandListenerImpl implements TreeWillExpandListener {
468 public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException {
469 TreePath path = event.getPath();
470 final Object lastPathComponent = path.getLastPathComponent();
471 if (!createdStatusSet.contains(lastPathComponent)) {
472 InfoNode node = (InfoNode)lastPathComponent;
476 createdStatusSet.add(node);
478 expandNode(node, dbmeta);
479 } catch (SQLException ex) {
480 throw new RuntimeException(ex);
485 public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException {
489 addTreeWillExpandListener(new TreeWillExpandListenerImpl());
490 this.dbmeta = dbmeta;
493 setRootVisible(true);
494 this.currentConnector = c;
497 File confFile = Bootstrap.getSystemFile("autoexpansion.tsv");
498 if (confFile.exists() && confFile.length() > 0) {
499 AnyAction aa = new AnyAction(this);
500 Scanner r = new Scanner(confFile);
502 while (r.hasNextLine()) {
503 final String line = r.nextLine();
504 if (line.matches("^\\s*#.*")) {
507 aa.doParallel("expandNodes", Arrays.asList(line.split("\t")));
513 } catch (IOException ex) {
514 throw new IllegalStateException(ex);
518 void expandNodes(List<String> a) {
519 long startTime = System.currentTimeMillis();
520 AnyAction aa = new AnyAction(this);
522 while (index < a.size()) {
523 final String s = a.subList(0, index + 1).toString();
524 for (int i = 0, n = getRowCount(); i < n; i++) {
527 target = getPathForRow(i);
528 } catch (IndexOutOfBoundsException ex) {
529 // FIXME when IndexOutOfBoundsException was thrown at expandNodes
533 if (target != null && target.toString().equals(s)) {
534 if (!isExpanded(target)) {
535 aa.doLater("expandLater", target);
536 Utilities.sleep(200L);
542 if (System.currentTimeMillis() - startTime > 5000L) {
548 // called by expandNodes
549 @SuppressWarnings("unused")
550 private void expandLater(TreePath parent) {
555 * Refreshes a node and its children.
558 void refresh(InfoNode node) {
559 if (dbmeta == null) {
562 node.removeAllChildren();
563 final DefaultTreeModel model = (DefaultTreeModel)getModel();
566 expandNode(node, dbmeta);
567 } catch (SQLException ex) {
568 throw new RuntimeException(ex);
576 * @throws SQLException
578 void expandNode(final InfoNode parent, final DatabaseMetaData dbmeta) throws SQLException {
579 if (parent.isLeaf()) {
582 final DefaultTreeModel model = (DefaultTreeModel)getModel();
583 final DefaultMutableTreeNode tmpNode = new DefaultMutableTreeNode(res.get("i.paren-in-processing"));
585 class NodeExpansionTask implements Runnable {
588 class Task1 implements Runnable {
591 model.insertNodeInto(tmpNode, parent, 0);
594 invokeLater(new Task1());
595 final List<InfoNode> children;
597 children = new ArrayList<InfoNode>(parent.createChildren(dbmeta));
598 } catch (SQLException ex) {
600 if (dbmeta.getConnection().isClosed())
602 } catch (SQLException exx) {
603 ex.setNextException(exx);
605 throw new RuntimeException(ex);
607 class Task2 implements Runnable {
610 for (InfoNode child : children) {
611 model.insertNodeInto(child, parent, parent.getChildCount());
613 model.removeNodeFromParent(tmpNode);
616 invokeLater(new Task2());
619 DaemonThreadFactory.execute(new NodeExpansionTask());
626 for (TreeWillExpandListener listener : getListeners(TreeWillExpandListener.class).clone()) {
627 removeTreeWillExpandListener(listener);
629 setModel(new DefaultTreeModel(null));
630 currentConnector = null;
632 if (log.isDebugEnabled()) {
633 log.debug("cleared");
639 private static final class ListMap extends LinkedHashMap<String, List<String>> {
645 void add(String key, String... values) {
646 if (get(key) == null) {
647 put(key, new ArrayList<String>());
649 for (String value : values) {
656 private static class Renderer extends DefaultTreeCellRenderer {
663 public Component getTreeCellRendererComponent(JTree tree,
670 super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
671 setIcon((value instanceof InfoNode) ? Utilities.getImageIcon(((InfoNode)value).getIconName()) : null);
672 if (value instanceof ColumnNode) {
673 if (showColumnNumber) {
674 TreePath path = tree.getPathForRow(row);
676 TreePath parent = path.getParentPath();
677 if (parent != null) {
678 final int index = row - tree.getRowForPath(parent);
679 setText(String.format("%d %s", index, getText()));
689 private abstract static class InfoNode extends DefaultMutableTreeNode {
691 InfoNode(Object userObject) {
692 super(userObject, true);
696 public boolean isLeaf() {
700 abstract protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException;
702 String getIconName() {
703 final String className = getClass().getName();
704 final String nodeType = className.replaceFirst(".+?([^\\$]+)Node$", "$1");
705 return "node-" + nodeType.toLowerCase() + ".png";
708 protected String getNodeFullName() {
709 return String.valueOf(userObject);
712 static List<TableTypeNode> getTableTypeNodes(DatabaseMetaData dbmeta,
714 String schema) throws SQLException {
715 List<String> tableTypes = new ArrayList<String>(DEFAULT_TABLE_TYPES);
717 ResultSet rs = dbmeta.getTableTypes();
720 final String tableType = rs.getString(1);
721 if (!DEFAULT_TABLE_TYPES.contains(tableType)) {
722 tableTypes.add(tableType);
728 } catch (SQLException ex) {
729 log.warn("getTableTypes at getTableTypeNodes", ex);
731 List<TableTypeNode> a = new ArrayList<TableTypeNode>();
732 for (final String tableType : tableTypes) {
733 TableTypeNode typeNode = new TableTypeNode(catalog, schema, tableType);
734 if (typeNode.hasItems(dbmeta)) {
743 private static class ConnectorNode extends InfoNode {
745 ConnectorNode(String name) {
750 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
751 List<InfoNode> a = new ArrayList<InfoNode>();
752 if (dbmeta.supportsCatalogsInDataManipulation()) {
753 ResultSet rs = dbmeta.getCatalogs();
756 a.add(new CatalogNode(rs.getString(1)));
761 } else if (dbmeta.supportsSchemasInDataManipulation()) {
762 ResultSet rs = dbmeta.getSchemas();
765 a.add(new SchemaNode(null, rs.getString(1)));
771 a.addAll(getTableTypeNodes(dbmeta, null, null));
778 private static final class CatalogNode extends InfoNode {
780 private final String name;
782 CatalogNode(String name) {
788 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
789 List<InfoNode> a = new ArrayList<InfoNode>();
790 if (dbmeta.supportsSchemasInDataManipulation()) {
791 ResultSet rs = dbmeta.getSchemas();
794 a.add(new SchemaNode(name, rs.getString(1)));
800 a.addAll(getTableTypeNodes(dbmeta, name, null));
807 private static final class SchemaNode extends InfoNode {
809 private final String catalog;
810 private final String schema;
812 SchemaNode(String catalog, String schema) {
814 this.catalog = catalog;
815 this.schema = schema;
819 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
820 List<InfoNode> a = new ArrayList<InfoNode>();
821 a.addAll(getTableTypeNodes(dbmeta, catalog, schema));
827 private static final class TableTypeNode extends InfoNode {
829 private static final String ICON_NAME_FORMAT = "node-tabletype-%s.png";
831 private final String catalog;
832 private final String schema;
833 private final String tableType;
835 TableTypeNode(String catalog, String schema, String tableType) {
837 this.catalog = catalog;
838 this.schema = schema;
839 this.tableType = tableType;
843 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
844 List<InfoNode> a = new ArrayList<InfoNode>();
845 ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
848 final String table = rs.getString(3);
849 final String type = rs.getString(4);
850 a.add(new TableNode(catalog, schema, table, type));
859 String getIconName() {
860 final String name = String.format(ICON_NAME_FORMAT, getUserObject());
861 if (getClass().getResource("icon/" + name) == null) {
862 return String.format(ICON_NAME_FORMAT, "");
867 boolean hasItems(DatabaseMetaData dbmeta) throws SQLException {
868 ResultSet rs = dbmeta.getTables(catalog, schema, null, new String[]{tableType});
878 static final class TableNode extends InfoNode {
880 private final String catalog;
881 private final String schema;
882 private final String name;
883 private final String tableType;
885 TableNode(String catalog, String schema, String name, String tableType) {
887 this.catalog = catalog;
888 this.schema = schema;
890 this.tableType = tableType;
894 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
895 List<InfoNode> a = new ArrayList<InfoNode>();
896 ResultSet rs = dbmeta.getColumns(catalog, schema, name, null);
899 a.add(new ColumnNode(rs.getString(4), rs.getString(6), rs.getInt(7), rs.getString(18), this));
908 public boolean isLeaf() {
909 if (TABLE_TYPE_SEQUENCE.equals(tableType)) {
916 protected String getNodeFullName() {
917 List<String> a = new ArrayList<String>();
918 if (catalog != null) {
921 if (schema != null) {
934 static final class ColumnNode extends InfoNode {
936 private final String name;
937 private final TableNode tableNode;
939 ColumnNode(String name, String type, int size, String nulls, TableNode tableNode) {
940 super(format(name, type, size, nulls));
941 setAllowsChildren(false);
943 this.tableNode = tableNode;
950 TableNode getTableNode() {
954 private static String format(String name, String type, int size, String nulls) {
955 final String nonNull = "NO".equals(nulls) ? " NOT NULL" : "";
956 return String.format("%s [%s(%d)%s]", name, type, size, nonNull);
960 public boolean isLeaf() {
965 protected List<InfoNode> createChildren(DatabaseMetaData dbmeta) throws SQLException {
970 protected String getNodeFullName() {
971 return String.format("%s.%s", tableNode.getNodeFullName(), name);