001: /*
002: * This is free software, licensed under the Gnu Public License (GPL)
003: * get a copy from <http://www.gnu.org/licenses/gpl.html>
004: *
005: * author: Henner Zeller <H.Zeller@acm.org>
006: */
007: package henplus.commands;
008:
009: import henplus.AbstractCommand;
010: import henplus.CommandDispatcher;
011: import henplus.HenPlus;
012: import henplus.Interruptable;
013: import henplus.SQLSession;
014: import henplus.SigIntHandler;
015:
016: import java.sql.DatabaseMetaData;
017: import java.sql.ResultSet;
018: import java.sql.SQLException;
019: import java.util.Iterator;
020: import java.util.Map;
021: import java.util.Set;
022: import java.util.List;
023: import java.util.ArrayList;
024: import java.util.SortedSet;
025: import java.util.StringTokenizer;
026: import java.util.TreeMap;
027: import java.util.TreeSet;
028:
029: import henplus.OutputDevice;
030:
031: /* fop --+
032: * abc--+ |
033: * xyz--| |
034: * blub --|
035: * foo
036: * |-- bar
037: * | |-- blah
038: * | `-- (foo) <-- cylic reference
039: * `-- baz
040: */
041:
042: /**
043: * creates a dependency graph.
044: */
045: public class TreeCommand extends AbstractCommand implements
046: Interruptable {
047: private final static int IMP_PRIMARY_KEY_TABLE = 3;
048: /** reference in exported/imported key */
049: private final static int EXP_FOREIGN_KEY_TABLE = 7;
050:
051: static final boolean verbose = false;
052:
053: private final ListUserObjectsCommand tableCompleter;
054: private volatile boolean interrupted;
055:
056: /**
057: * A node in a cyclic graph.
058: */
059: private static abstract class Node implements Comparable {
060: private final Set/*<Node>*/_children;
061: private int _displayDepth;
062:
063: protected Node() {
064: _children = new TreeSet();
065: _displayDepth = -1;
066: }
067:
068: public Node add(Node n) {
069: _children.add(n);
070: return n;
071: }
072:
073: boolean markDepth(int target, int current) {
074: if (target == current) {
075: if (_displayDepth < 0) {
076: _displayDepth = current;
077: return true;
078: }
079: return false;
080: }
081: boolean anyChange = false;
082: final Iterator it = _children.iterator();
083: while (it.hasNext()) {
084: Node n = (Node) it.next();
085: anyChange |= n.markDepth(target, current + 1);
086: }
087: return anyChange;
088: }
089:
090: public void markDepths() {
091: _displayDepth = 0;
092: for (int depth = 1; markDepth(depth, 0); ++depth)
093: ;
094: }
095:
096: public void print(OutputDevice out, int indentCount) {
097: StringBuffer indent = new StringBuffer();
098: for (int i = 0; i < indentCount; ++i) {
099: indent.append(" ");
100: }
101: print(0, new TreeSet(), new StringBuffer(), indent
102: .toString(), out);
103: }
104:
105: private void print(int depth, SortedSet alreadyPrinted,
106: StringBuffer currentIndent, String indentString,
107: OutputDevice out) {
108: final String name = getName();
109: if (depth != 0) {
110: out.print("-- ");
111: boolean cyclic = (depth != _displayDepth)
112: || alreadyPrinted.contains(name);
113: if (cyclic) {
114: out.print("(" + name + ")");
115: } else {
116: out.print(name);
117: }
118: out.println();
119: if (cyclic) {
120: return;
121: }
122: }
123: alreadyPrinted.add(name);
124:
125: int remaining = _children.size();
126: if (remaining > 0) {
127: int previousLength = currentIndent.length();
128: currentIndent.append(indentString);
129: Iterator it = _children.iterator();
130: while (it.hasNext()) {
131: Node n = (Node) it.next();
132: out.print(String.valueOf(currentIndent));
133: out.print((remaining == 1) ? "`" : "|");
134: n.print(depth + 1, alreadyPrinted, currentIndent,
135: (remaining == 1) ? " " : "| ", out);
136: --remaining;
137: }
138: currentIndent.setLength(previousLength);
139: }
140: }
141:
142: public int printReverse(OutputDevice out) {
143: List result = new ArrayList();
144: printReverse(0, new TreeSet(), result, "", false);
145: Iterator it = result.iterator();
146: int maxLen = 0;
147: while (it.hasNext()) {
148: String line = (String) it.next();
149: if (line.length() > maxLen)
150: maxLen = line.length();
151: }
152: it = result.iterator();
153: while (it.hasNext()) {
154: String line = (String) it.next();
155: int len = line.length();
156: for (int i = len; i < maxLen; ++i) {
157: out.print(" ");
158: }
159: out.println(line);
160: }
161: return maxLen;
162: }
163:
164: private int printReverse(int depth, SortedSet alreadyPrinted,
165: List output, String indentString, boolean isLast) {
166: String name = getName();
167: boolean cyclic = (depth != _displayDepth)
168: || alreadyPrinted.contains(name);
169: String printName = cyclic ? "(" + name + ")--" : name
170: + "--";
171: int myIndent = indentString.length() + printName.length();
172: int maxIndent = myIndent;
173:
174: if (!cyclic) {
175: alreadyPrinted.add(name);
176: int remaining = _children.size();
177: if (remaining > 0) {
178: Iterator it = _children.iterator();
179: boolean isFirst = true;
180: while (it.hasNext()) {
181: Node n = (Node) it.next();
182: int nIndent;
183: nIndent = n.printReverse(depth + 1,
184: alreadyPrinted, output,
185: (depth == 0) ? "" : indentString
186: + " |", isFirst);
187: if (nIndent > maxIndent)
188: maxIndent = nIndent;
189: --remaining;
190: isFirst = false;
191: }
192: }
193: }
194:
195: if (depth != 0) {
196: String outputString = printName + (isLast ? "\\" : "|")
197: + indentString;
198: output.add(outputString);
199: }
200: return maxIndent;
201: }
202:
203: public int compareTo(Object o) {
204: Node other = (Node) o;
205: return getName().compareTo(other.getName());
206: }
207:
208: /**
209: * This is what we need to print the stuff..
210: */
211: public abstract String getName();
212: }
213:
214: /**
215: * the entity is simply represented as String.
216: */
217: private static class StringNode extends Node {
218: private final String _name;
219:
220: public StringNode(String s) {
221: _name = s;
222: }
223:
224: public String getName() {
225: return _name;
226: }
227: }
228:
229: public TreeCommand(ListUserObjectsCommand tc) {
230: tableCompleter = tc;
231: }
232:
233: /**
234: * returns the command-strings this command can handle.
235: */
236: public String[] getCommandList() {
237: return new String[] { "tree-view" };
238: }
239:
240: /**
241: * execute the command given.
242: */
243: public int execute(SQLSession session, String cmd, String param) {
244: final StringTokenizer st = new StringTokenizer(param);
245: final int argc = st.countTokens();
246: if (argc != 1) {
247: return SYNTAX_ERROR;
248: }
249:
250: boolean correctName = true;
251: String tabName = (String) st.nextElement();
252: if (tabName.startsWith("\"")) {
253: tabName = stripQuotes(tabName);
254: correctName = false;
255: }
256: if (correctName) {
257: String alternative = tableCompleter
258: .correctTableName(tabName);
259: if (alternative != null && !alternative.equals(tabName)) {
260: tabName = alternative;
261: }
262: }
263: String schema = null; // fixme: determine
264:
265: try {
266: long startTime = System.currentTimeMillis();
267: final DatabaseMetaData dbMeta = session.getConnection()
268: .getMetaData();
269:
270: interrupted = false;
271: SigIntHandler.getInstance().pushInterruptable(this );
272:
273: // build a tree of all tables I depend on..
274: Node myParents = buildTree(new ReferenceMetaDataSource() {
275: public ResultSet getReferenceMetaData(String schema,
276: String table) throws SQLException {
277: return dbMeta.getImportedKeys(null, schema, table);
278: }
279: }, IMP_PRIMARY_KEY_TABLE, new TreeMap(), schema, tabName);
280: if (interrupted)
281: return SUCCESS;
282: myParents.markDepths();
283:
284: // build a tree of all tables that depend on me ...
285: Node myChilds = buildTree(new ReferenceMetaDataSource() {
286: public ResultSet getReferenceMetaData(String schema,
287: String table) throws SQLException {
288: return dbMeta.getExportedKeys(null, schema, table);
289: }
290: }, EXP_FOREIGN_KEY_TABLE, new TreeMap(), schema, tabName);
291: if (interrupted)
292: return SUCCESS;
293: myChilds.markDepths();
294:
295: int reversIndent = myParents.printReverse(HenPlus.out());
296:
297: int tabLen = tabName.length();
298: int startPos = reversIndent - tabLen / 2;
299: if (startPos < 0)
300: startPos = 0;
301: for (int i = 0; i < startPos; ++i)
302: HenPlus.out().print(" ");
303: HenPlus.out().attributeBold();
304: HenPlus.out().println(tabName);
305: HenPlus.out().attributeReset();
306:
307: myChilds.print(HenPlus.out(), startPos + tabLen / 2);
308:
309: TimeRenderer.printTime(System.currentTimeMillis()
310: - startTime, HenPlus.msg());
311: HenPlus.msg().println();
312: } catch (Exception e) {
313: HenPlus.msg().println(
314: "problem getting database meta data: "
315: + e.getMessage());
316: return EXEC_FAILED;
317: }
318: return SUCCESS;
319: }
320:
321: private interface ReferenceMetaDataSource {
322: ResultSet getReferenceMetaData(String schema, String table)
323: throws SQLException;
324: }
325:
326: /**
327: * build a subtree from the MetaData for the table with the given name.
328: * If this node already exists (because of a cyclic dependency),
329: * return that. recursively called to build the whole tree.
330: * This determines its refernece data from the ReferenceMetaDataSource
331: * 'lambda' that either wraps getImportedKeys() or getExportedKeys().
332: * The 'sourceColumn' defines the column in which the appropriate
333: * table name is.
334: */
335: private Node buildTree(ReferenceMetaDataSource source,
336: int sourceColumn, Map knownNodes, String schema,
337: String tabName) throws SQLException {
338: if (knownNodes.containsKey(tabName)) {
339: return (Node) knownNodes.get(tabName);
340: }
341:
342: Node n = new StringNode(tabName);
343: knownNodes.put(tabName, n);
344: ResultSet rset = null;
345: try {
346: rset = source.getReferenceMetaData(schema, tabName);
347: // read this into a list to avoid recursive calls to MetaData
348: // which some JDBC-drivers don't like..
349: List refTables = new ArrayList();
350: while (rset.next()) {
351: String referencingTable = rset.getString(sourceColumn);
352: refTables.add(referencingTable);
353: }
354:
355: Iterator it = refTables.iterator();
356: while (it.hasNext()) {
357: String referencingTable = (String) it.next();
358: if (interrupted)
359: return null;
360: n.add(buildTree(source, sourceColumn, knownNodes,
361: schema, referencingTable));
362: }
363: } finally {
364: if (rset != null) {
365: try {
366: rset.close();
367: } catch (Exception e) {
368: }
369: }
370: }
371: return n;
372: }
373:
374: /**
375: * complete the table name.
376: */
377: public Iterator complete(CommandDispatcher disp,
378: String partialCommand, String lastWord) {
379: StringTokenizer st = new StringTokenizer(partialCommand);
380: st.nextElement(); // skip cmd.
381: // we accept only one argument.
382: if (lastWord.startsWith("\"")) {
383: lastWord = lastWord.substring(1);
384: }
385: return tableCompleter.completeTableName(HenPlus.getInstance()
386: .getCurrentSession(), lastWord);
387: }
388:
389: private String stripQuotes(String value) {
390: if (value.startsWith("\"") && value.endsWith("\"")) {
391: value = value.substring(1, value.length() - 1);
392: }
393: return value;
394: }
395:
396: /** interrupt interface */
397: public void interrupt() {
398: interrupted = true;
399: }
400:
401: /**
402: * return a descriptive string.
403: */
404: public String getShortDescription() {
405: return "tree representation of connected tables";
406: }
407:
408: public String getSynopsis(String cmd) {
409: return cmd + " <tablename>";
410: }
411:
412: public String getLongDescription(String cmd) {
413: String dsc = null;
414: dsc = "\tShow tables, that are connected via foreign keys in a\n"
415: + "\ttree like manner. This is very helpful in exploring\n"
416: + "\tcomplicated data structures or simply check if all\n"
417: + "\tforeign keys are applied. This command works of course\n"
418: + "\tonly with databases that support foreign keys.\n"
419: + "\tInvoke on the toplevel table you are interested in\n"
420: + "\tExample:\n"
421: + "\tConsider tables 'bar' and 'baz' that have a foreign key\n"
422: + "\ton the table 'foo'. Further a table 'blah', that references\n"
423: + "\t'bar'. The table 'foo' in turn references 'bar', thus\n"
424: + "\tcyclicly referencing itself. Invoking tree-view on 'foo'\n"
425: + "\twould be represented as\n"
426: + "\t foo\n"
427: + "\t |-- bar\n"
428: + "\t | |-- blah\n"
429: + "\t | `-- (foo) <-- cylic/already printed reference\n"
430: + "\t `-- baz\n"
431: + "\tSo in order to limit the potential cyclic graph in the\n"
432: + "\ttree view from infinite to finite, cyclic nodes or nodes already\n"
433: + "\tdisplayed unfolded are shown in parenthesis.";
434: return dsc;
435: }
436: }
437:
438: /*
439: * Local variables:
440: * c-basic-offset: 4
441: * compile-command: "ant -emacs -find build.xml"
442: * End:
443: */
|