001: /*
002: * This is free software, licensed under the Gnu Public License (GPL) get a copy from
003: * <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: import henplus.util.StringAppender;
016: import henplus.view.Column;
017: import henplus.view.ColumnMetaData;
018: import henplus.view.TableRenderer;
019:
020: import java.sql.DatabaseMetaData;
021: import java.sql.ResultSet;
022: import java.sql.SQLException;
023: import java.util.ArrayList;
024: import java.util.HashMap;
025: import java.util.HashSet;
026: import java.util.Iterator;
027: import java.util.List;
028: import java.util.Map;
029: import java.util.NoSuchElementException;
030: import java.util.Set;
031: import java.util.StringTokenizer;
032:
033: /**
034: * document me.
035: */
036: public class DescribeCommand extends AbstractCommand implements
037: Interruptable {
038: private final static String[] LIST_TABLES = { "TABLE", "VIEW" };
039: private final static ColumnMetaData[] DESC_META;
040: static {
041: DESC_META = new ColumnMetaData[9];
042: DESC_META[0] = new ColumnMetaData("#",
043: ColumnMetaData.ALIGN_RIGHT);
044: DESC_META[1] = new ColumnMetaData("table");
045: DESC_META[2] = new ColumnMetaData("column");
046: DESC_META[3] = new ColumnMetaData("type");
047: DESC_META[4] = new ColumnMetaData("null");
048: DESC_META[5] = new ColumnMetaData("default");
049: DESC_META[6] = new ColumnMetaData("pk");
050: DESC_META[7] = new ColumnMetaData("fk");
051: DESC_META[8] = new ColumnMetaData("remark",
052: ColumnMetaData.ALIGN_LEFT, 60);
053: }
054:
055: private boolean interrupted;
056: private boolean verbose;
057: private final ListUserObjectsCommand tableCompleter;
058:
059: public DescribeCommand(ListUserObjectsCommand tc) {
060: tableCompleter = tc;
061: }
062:
063: /**
064: * returns the command-strings this command can handle.
065: */
066: public String[] getCommandList() {
067: return new String[] { "describe", "idescribe" };
068: }
069:
070: /**
071: * execute the command given.
072: */
073: public int execute(SQLSession session, String cmd, String param) {
074: // make use of properties for these properties?
075: // (since the options just toggle, this may be convenient)
076: boolean showDescriptions = true;
077: boolean showIndex = "idescribe".equals(cmd);
078: boolean showTime = true;
079:
080: final StringTokenizer st = new StringTokenizer(param);
081: if (st.countTokens() < 1)
082: return SYNTAX_ERROR;
083:
084: // this was a flag to ensure that all options come before the tablenames
085: // can probably be removed...
086: boolean more_options = true;
087: while (st.hasMoreTokens()) {
088: String tabName = st.nextToken();
089: if (more_options && tabName.startsWith("-")) {
090: if (tabName.indexOf('i') > -1)
091: showIndex = !showIndex;
092: if (tabName.indexOf('v') > -1)
093: showDescriptions = !showDescriptions;
094: if (tabName.indexOf('t') > -1)
095: showTime = !showTime;
096: } else {
097: // more_options = false; // options can stand at every position --> toggle
098: boolean correctName = true;
099:
100: if (tabName.startsWith("\"")) {
101: tabName = stripQuotes(tabName);
102: correctName = false;
103: }
104:
105: // separate schama and table.
106: String schema = null;
107: int schemaDelim = tabName.indexOf('.');
108: if (schemaDelim > 0) {
109: schema = tabName.substring(0, schemaDelim);
110: tabName = tabName.substring(schemaDelim + 1);
111: }
112:
113: // FIXME: provide correct name as well for schema!
114: if (correctName) {
115: String alternative = tableCompleter
116: .correctTableName(tabName);
117: if (alternative != null
118: && !alternative.equals(tabName)) {
119: tabName = alternative;
120: HenPlus.out().println(
121: "describing table: '" + tabName
122: + "' (corrected name)");
123: }
124: }
125:
126: ResultSet rset = null;
127: Set doubleCheck = new HashSet();
128: try {
129: interrupted = false;
130: SigIntHandler.getInstance().pushInterruptable(this );
131: boolean anyLeftArrow = false;
132: boolean anyRightArrow = false;
133: long startTime = System.currentTimeMillis();
134: String catalog = session.getConnection()
135: .getCatalog();
136: String description = null;
137: String tableType = null;
138:
139: if (interrupted)
140: return SUCCESS;
141:
142: DatabaseMetaData meta = session.getConnection()
143: .getMetaData();
144: for (int i = 0; i < DESC_META.length; ++i) {
145: DESC_META[i].resetWidth();
146: }
147:
148: rset = meta.getTables(catalog, schema, tabName,
149: LIST_TABLES);
150: if (rset != null && rset.next()) {
151: tableType = rset.getString(4);
152: description = rset.getString(5); // remark
153: }
154: rset.close();
155:
156: /*
157: * get primary keys.
158: */
159: if (interrupted)
160: return SUCCESS;
161: Map pks = new HashMap();
162: rset = meta.getPrimaryKeys(null, schema, tabName);
163: if (rset != null)
164: while (!interrupted && rset.next()) {
165: String col = rset.getString(4);
166: int pkseq = rset.getInt(5);
167: String pkname = rset.getString(6);
168: String desc = (pkname != null) ? pkname
169: : "*";
170: if (pkseq > 1) {
171: desc = StringAppender.getInstance()
172: .append(desc).append("{")
173: .append(pkseq).append("}")
174: .toString();
175: // desc += "{" + pkseq + "}";
176: }
177: pks.put(col, desc);
178: }
179: rset.close();
180:
181: /*
182: * get referenced primary keys.
183: */
184: if (interrupted)
185: return SUCCESS;
186: rset = meta.getExportedKeys(null, schema, tabName);
187: if (rset != null)
188: while (!interrupted && rset.next()) {
189: String col = rset.getString(4);
190: String fktable = rset.getString(7);
191: String fkcolumn = rset.getString(8);
192: fktable = StringAppender.getInstance()
193: .append(fktable).append("(")
194: .append(fkcolumn).append(")")
195: .toString();
196: String desc = (String) pks.get(col);
197: desc = (desc == null) ? StringAppender
198: .start(" <- ").append(fktable)
199: .toString() : StringAppender.start(
200: desc).append("\n <- ").append(
201: fktable).toString();
202: anyLeftArrow = true;
203: pks.put(col, desc);
204: }
205: rset.close();
206:
207: /*
208: * get foreign keys.
209: */
210: if (interrupted)
211: return SUCCESS;
212: Map fks = new HashMap();
213:
214: // some jdbc version 2 drivers (connector/j) have problems with foreign keys...
215: try {
216: rset = meta.getImportedKeys(null, schema,
217: tabName);
218: } catch (NoSuchElementException e) {
219: if (verbose) {
220: HenPlus.msg().println(
221: "Database problem reading meta data: "
222: + e);
223: }
224: }
225: if (rset != null) {
226: while (!interrupted && rset.next()) {
227: String table = rset.getString(3);
228: String pkcolumn = rset.getString(4);
229: table = table + "(" + pkcolumn + ")";
230: String col = rset.getString(8);
231: String fkname = rset.getString(12);
232: String desc = (fkname != null) ? StringAppender
233: .start(fkname).append("\n -> ")
234: .toString()
235: : " -> ";
236: desc += table;
237: anyRightArrow = true;
238: fks.put(col, desc);
239: }
240: }
241: rset.close();
242:
243: HenPlus.out().println(
244: ("VIEW".equals(tableType) ? "View: "
245: : "Table: ")
246: + tabName);
247: if (description != null) {
248: HenPlus.out().println(description);
249: }
250:
251: if (catalog != null) {
252: HenPlus.msg().println("catalog: " + catalog);
253: }
254: if (anyLeftArrow) {
255: HenPlus.msg().println(" '<-' : referenced by");
256: }
257: if (anyRightArrow) {
258: HenPlus.msg().println(" '->' : referencing");
259: }
260:
261: /*
262: * if all columns belong to the same table name, then
263: * don't report it. A different table
264: * name may only occur in rare circumstance like object
265: * oriented databases.
266: */
267: boolean allSameTableName = true;
268:
269: /*
270: * build up actual describe table.
271: */
272: if (interrupted)
273: return SUCCESS;
274:
275: rset = meta.getColumns(catalog, schema, tabName,
276: null);
277: List rows = new ArrayList();
278: int colNum = 0;
279: boolean anyDescription = false;
280: if (rset != null) {
281: while (!interrupted && rset.next()) {
282: final Column[] row = new Column[9];
283: row[0] = new Column(++colNum);
284: final String this TabName = rset
285: .getString(3);
286: row[1] = new Column(this TabName);
287: allSameTableName &= tabName
288: .equals(this TabName);
289: final String colname = rset.getString(4);
290: if (doubleCheck.contains(colname)) {
291: continue;
292: }
293: doubleCheck.add(colname);
294: row[2] = new Column(colname);
295: String type = rset.getString(6);
296: final int colSize = rset.getInt(7);
297: if (colSize > 0) {
298: type = StringAppender.start(type)
299: .append("(").append(colSize)
300: .append(")").toString();
301: }
302:
303: row[3] = new Column(type);
304: final String defaultVal = rset
305: .getString(13);
306: row[4] = new Column(rset.getString(18));
307: // oracle appends newline to default values for some reason.
308: row[5] = new Column(
309: ((defaultVal != null) ? defaultVal
310: .trim() : null));
311: final String pkdesc = (String) pks
312: .get(colname);
313: row[6] = new Column(
314: (pkdesc != null) ? pkdesc : "");
315: final String fkdesc = (String) fks
316: .get(colname);
317: row[7] = new Column(
318: (fkdesc != null) ? fkdesc : "");
319:
320: final String colDesc = (showDescriptions) ? rset
321: .getString(12)
322: : null;
323: row[8] = new Column(colDesc);
324: anyDescription |= (colDesc != null);
325: rows.add(row);
326: }
327: }
328: rset.close();
329:
330: /*
331: * we render the table now, since we only know now,
332: * whether we will show the first
333: * column and the description column or not.
334: */
335: DESC_META[1].setDisplay(!allSameTableName);
336: DESC_META[8].setDisplay(anyDescription);
337: TableRenderer table = new TableRenderer(DESC_META,
338: HenPlus.out());
339: Iterator it = rows.iterator();
340: while (it.hasNext()) {
341: table.addRow((Column[]) it.next());
342: }
343: table.closeTable();
344:
345: if (interrupted)
346: return SUCCESS;
347:
348: if (showIndex) {
349: showIndexInformation(tabName, schema, meta);
350: }
351:
352: if (showTime) {
353: TimeRenderer.printTime(System
354: .currentTimeMillis()
355: - startTime, HenPlus.out());
356: HenPlus.out().println();
357: }
358:
359: } catch (Exception e) {
360: if (verbose)
361: e.printStackTrace();
362: String ex = (e.getMessage() != null) ? e
363: .getMessage().trim() : e.toString();
364: HenPlus.msg()
365: .println(
366: "Database problem reading meta data: "
367: + ex);
368: return EXEC_FAILED;
369: } finally {
370: if (rset != null) {
371: try {
372: rset.close();
373: } catch (Exception e) {
374: }
375: }
376: }
377:
378: }
379: }
380: return SUCCESS;
381: }
382:
383: /**
384: * @param tabName
385: * @param schema
386: * @param meta
387: * @return @throws SQLException
388: */
389: private void showIndexInformation(String tabName, String schema,
390: DatabaseMetaData meta) throws SQLException {
391: ResultSet rset;
392: HenPlus.out().println("index information:");
393: boolean anyIndex = false;
394: rset = meta.getIndexInfo(null, schema, tabName, false, true);
395: if (rset != null)
396: while (!interrupted && rset.next()) {
397: boolean nonUnique;
398: String idxName = null;
399: nonUnique = rset.getBoolean(4);
400: idxName = rset.getString(6);
401: if (idxName == null)
402: continue; // statistics, otherwise.
403: // output part.
404: anyIndex = true;
405: HenPlus.out().print("\t");
406: if (!nonUnique)
407: HenPlus.out().print("unique ");
408: HenPlus.out().print("index " + idxName);
409: String colName = rset.getString(9);
410: // work around postgres-JDBC-driver bug:
411: if (colName != null && colName.length() > 0) {
412: HenPlus.out().print(" on " + colName);
413: }
414: HenPlus.out().println();
415: }
416: rset.close();
417: if (!anyIndex) {
418: HenPlus.out().println("\t<none>");
419: }
420: }
421:
422: /**
423: * complete the table name.
424: */
425: public Iterator complete(CommandDispatcher disp,
426: String partialCommand, String lastWord) {
427: StringTokenizer st = new StringTokenizer(partialCommand);
428: st.nextElement(); // consume first element.
429: if (lastWord.startsWith("\"")) {
430: lastWord = lastWord.substring(1);
431: }
432: return tableCompleter.completeTableName(HenPlus.getInstance()
433: .getCurrentSession(), lastWord);
434: }
435:
436: private String stripQuotes(String value) {
437: if (value.startsWith("\"") && value.endsWith("\"")) {
438: value = value.substring(1, value.length() - 1);
439: }
440: return value;
441: }
442:
443: //-- Interruptable interface
444: public synchronized void interrupt() {
445: interrupted = true;
446: }
447:
448: /**
449: * return a descriptive string.
450: */
451: public String getShortDescription() {
452: return "describe a database object";
453: }
454:
455: public String getSynopsis(String cmd) {
456: return cmd + " [options] <tablenames>";
457: }
458:
459: public String getLongDescription(String cmd) {
460: String dsc;
461: dsc = "\tDescribe the meta information of the named user object\n"
462: + "\t(only tables for now). The name you type is case sensitive\n"
463: + "\tbut henplus tries its best to correct it.\n"
464: + "\tThe 'describe' command just describes the table, the\n"
465: + "\t'idescribe' command determines the index information as\n"
466: + "\twell; some databases are really slow in this, so this is\n"
467: + "\tan extra command"
468: // TODO: add getOptions() to Command-Interface?
469: + "\n\n\tRecognized options are:\n"
470: + "\t -i show index information (same as idescribe)\n"
471: + "\t -v show column descriptions"
472: + "\n\n\tIf an option is positioned between two tablenames, its current state is toggled."
473: + "\n";
474: return dsc;
475: }
476:
477: }
478:
479: /* Emacs:
480: * Local variables:
481: * c-basic-offset: 4
482: * tab-width: 8
483: * indent-tabs-mode: nil
484: * compile-command: "ant -emacs -find build.xml"
485: * End:
486: * vi:set tabstop=8 shiftwidth=4 nowrap:
487: */
|