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: * $Id: SQLSession.java,v 1.33 2005/03/24 13:57:46 hzeller Exp $
005: * author: Henner Zeller <H.Zeller@acm.org>
006: */
007: package henplus;
008:
009: import henplus.property.BooleanPropertyHolder;
010: import henplus.property.EnumeratedPropertyHolder;
011: import henplus.sqlmodel.Table;
012:
013: import java.io.BufferedReader;
014: import java.io.IOException;
015: import java.io.InputStreamReader;
016: import java.sql.Connection;
017: import java.sql.DatabaseMetaData;
018: import java.sql.Driver;
019: import java.sql.DriverManager;
020: import java.sql.SQLException;
021: import java.sql.Statement;
022: import java.util.Properties;
023: import java.util.HashMap;
024: import java.util.Iterator;
025: import java.util.Map;
026: import java.util.SortedSet;
027:
028: /**
029: * a SQL session.
030: */
031: public class SQLSession implements Interruptable {
032: private long _connectTime;
033: private long _statementCount;
034: private String _url;
035: private String _username;
036: private String _password;
037: private String _databaseInfo;
038: private Connection _conn;
039: private SQLMetaData _metaData;
040:
041: private final PropertyRegistry _propertyRegistry;
042: private volatile boolean _interrupted;
043:
044: /**
045: * creates a new SQL session. Open the database connection, initializes
046: * the readline library
047: */
048: public SQLSession(String url, String user, String password)
049: throws IllegalArgumentException, ClassNotFoundException,
050: SQLException, IOException {
051: _statementCount = 0;
052: _conn = null;
053: _url = url;
054: _username = user;
055: _password = password;
056: _propertyRegistry = new PropertyRegistry();
057:
058: Driver driver = null;
059: //HenPlus.msg().println("connect to '" + url + "'");
060: driver = DriverManager.getDriver(url);
061:
062: HenPlus.msg().println("HenPlus II connecting ");
063: HenPlus.msg().println(" url '" + url + '\'');
064: HenPlus.msg().println(
065: " driver version " + driver.getMajorVersion() + "."
066: + driver.getMinorVersion());
067: connect();
068:
069: int currentIsolation = Connection.TRANSACTION_NONE;
070: DatabaseMetaData meta = _conn.getMetaData();
071: _databaseInfo = (meta.getDatabaseProductName() + " - " + meta
072: .getDatabaseProductVersion());
073: HenPlus.msg().println(" " + _databaseInfo);
074: try {
075: if (meta.supportsTransactions()) {
076: currentIsolation = _conn.getTransactionIsolation();
077: } else {
078: HenPlus.msg().println("no transactions.");
079: }
080: _conn.setAutoCommit(false);
081: } catch (SQLException ignore_me) {
082: }
083:
084: printTransactionIsolation(meta, Connection.TRANSACTION_NONE,
085: "No Transaction", currentIsolation);
086: printTransactionIsolation(meta,
087: Connection.TRANSACTION_READ_UNCOMMITTED,
088: "read uncommitted", currentIsolation);
089: printTransactionIsolation(meta,
090: Connection.TRANSACTION_READ_COMMITTED,
091: "read committed", currentIsolation);
092: printTransactionIsolation(meta,
093: Connection.TRANSACTION_REPEATABLE_READ,
094: "repeatable read", currentIsolation);
095: printTransactionIsolation(meta,
096: Connection.TRANSACTION_SERIALIZABLE, "serializable",
097: currentIsolation);
098:
099: Map availableIsolations = new HashMap();
100: addAvailableIsolation(availableIsolations, meta,
101: Connection.TRANSACTION_NONE, "none");
102: addAvailableIsolation(availableIsolations, meta,
103: Connection.TRANSACTION_READ_UNCOMMITTED,
104: "read-uncommitted");
105: addAvailableIsolation(availableIsolations, meta,
106: Connection.TRANSACTION_READ_COMMITTED, "read-committed");
107: addAvailableIsolation(availableIsolations, meta,
108: Connection.TRANSACTION_REPEATABLE_READ,
109: "repeatable-read");
110: addAvailableIsolation(availableIsolations, meta,
111: Connection.TRANSACTION_SERIALIZABLE, "serializable");
112:
113: _propertyRegistry.registerProperty("auto-commit",
114: new AutoCommitProperty());
115: _propertyRegistry.registerProperty("read-only",
116: new ReadOnlyProperty());
117: _propertyRegistry.registerProperty("isolation-level",
118: new IsolationLevelProperty(availableIsolations,
119: currentIsolation));
120: }
121:
122: private void printTransactionIsolation(DatabaseMetaData meta,
123: int iLevel, String descript, int current)
124: throws SQLException {
125: if (meta.supportsTransactionIsolationLevel(iLevel)) {
126: HenPlus
127: .msg()
128: .println(
129: " "
130: + descript
131: + ((current == iLevel) ? " *" : " "));
132: }
133: }
134:
135: private void addAvailableIsolation(Map result,
136: DatabaseMetaData meta, int iLevel, String key)
137: throws SQLException {
138: if (meta.supportsTransactionIsolationLevel(iLevel)) {
139: result.put(key, new Integer(iLevel));
140: }
141: }
142:
143: public PropertyRegistry getPropertyRegistry() {
144: return _propertyRegistry;
145: }
146:
147: public String getDatabaseInfo() {
148: return _databaseInfo;
149: }
150:
151: public String getURL() {
152: return _url;
153: }
154:
155: public SQLMetaData getMetaData(SortedSet/*<String>*/tableNames) {
156: if (_metaData == null) {
157: _metaData = new SQLMetaDataBuilder().getMetaData(this ,
158: tableNames);
159: }
160: return _metaData;
161: }
162:
163: public Table getTable(String tableName) {
164: return new SQLMetaDataBuilder().getTable(this , tableName);
165: }
166:
167: public boolean printMessages() {
168: return !(HenPlus.getInstance().getDispatcher().isInBatch());
169: }
170:
171: public void print(String msg) {
172: if (printMessages())
173: HenPlus.msg().print(msg);
174: }
175:
176: public void println(String msg) {
177: if (printMessages())
178: HenPlus.msg().println(msg);
179: }
180:
181: public void connect() throws SQLException, IOException {
182: /*
183: * close old connection ..
184: */
185: if (_conn != null) {
186: try {
187: _conn.close();
188: } catch (Throwable t) { /* ignore */
189: }
190: _conn = null;
191: }
192:
193: Properties props = new Properties();
194: /*
195: * FIXME
196: * make generic plugin for specific database drivers that handle
197: * the specific stuff. For now this is a quick hack.
198: */
199: if (_url.startsWith("jdbc:oracle:")) {
200: /* this is needed to make comment in oracle show up in
201: * the remarks
202: * http://forums.oracle.com/forums/thread.jsp?forum=99&thread=225790
203: */
204: props.setProperty("remarksReporting", "true");
205: }
206:
207: /* try to connect directly with the url. Several JDBC-Drivers
208: * allow to embed the username and password directly in the URL.
209: */
210: if (_username == null || _password == null) {
211: try {
212: _conn = DriverManager.getConnection(_url, props);
213: } catch (SQLException e) {
214: HenPlus.msg().println(e.getMessage());
215: // only query terminals.
216: if (HenPlus.msg().isTerminal()) {
217: promptUserPassword();
218: }
219: }
220: }
221:
222: if (_conn == null) {
223: _conn = DriverManager.getConnection(_url, _username,
224: _password);
225: }
226:
227: if (_conn != null && _username == null) {
228: try {
229: DatabaseMetaData meta = _conn.getMetaData();
230: if (meta != null) {
231: _username = meta.getUserName();
232: }
233: } catch (Exception e) {
234: /* ok .. at least I tried */
235: }
236: }
237: _connectTime = System.currentTimeMillis();
238: }
239:
240: private void promptUserPassword() throws IOException {
241: HenPlus.msg()
242: .println("============ authorization required ===");
243: BufferedReader input = new BufferedReader(
244: new InputStreamReader(System.in));
245: _interrupted = false;
246: try {
247: SigIntHandler.getInstance().pushInterruptable(this );
248: HenPlus.getInstance();
249: HenPlus.msg().print("Username: ");
250: _username = input.readLine();
251: if (_interrupted) {
252: throw new IOException("connect interrupted ..");
253: }
254: _password = promptPassword("Password: ");
255: if (_interrupted) {
256: throw new IOException("connect interrupted ..");
257: }
258: } finally {
259: SigIntHandler.getInstance().popInterruptable();
260: }
261: }
262:
263: /**
264: * This is after a hack found in
265: * http://java.sun.com/features/2002/09/pword_mask.html
266: */
267: private String promptPassword(String prompt) throws IOException {
268: String password = "";
269: PasswordEraserThread maskingthread = new PasswordEraserThread(
270: prompt);
271: try {
272: byte lineBuffer[] = new byte[64];
273: maskingthread.start();
274: for (;;) {
275: if (_interrupted) {
276: break;
277: }
278:
279: maskingthread.goOn();
280: int byteCount = System.in.read(lineBuffer);
281: /*
282: * hold on as soon as the system call returnes. Usually,
283: * this is because we read the newline.
284: */
285: maskingthread.holdOn();
286:
287: for (int i = 0; i < byteCount; ++i) {
288: char c = (char) lineBuffer[i];
289: if (c == '\r') {
290: c = (char) lineBuffer[++i];
291: if (c == '\n') {
292: return password;
293: } else {
294: continue;
295: }
296: } else if (c == '\n') {
297: return password;
298: } else {
299: password += c;
300: }
301: }
302: }
303: } finally {
304: maskingthread.done();
305: }
306:
307: return password;
308: }
309:
310: // -- Interruptable interface
311: public void interrupt() {
312: _interrupted = true;
313: HenPlus.msg().attributeBold();
314: HenPlus.msg().println(" interrupted; press [RETURN]");
315: HenPlus.msg().attributeReset();
316: }
317:
318: /**
319: * return username, if known.
320: */
321: public String getUsername() {
322: return _username;
323: }
324:
325: public long getUptime() {
326: return System.currentTimeMillis() - _connectTime;
327: }
328:
329: public long getStatementCount() {
330: return _statementCount;
331: }
332:
333: public void close() {
334: try {
335: getConnection().close();
336: _conn = null;
337: } catch (Exception e) {
338: HenPlus.msg().println(e.toString()); // don't care
339: }
340: }
341:
342: /**
343: * returns the current connection of this session.
344: */
345: public Connection getConnection() {
346: return _conn;
347: }
348:
349: public Statement createStatement() {
350: Statement result = null;
351: int retries = 2;
352: try {
353: if (_conn.isClosed()) {
354: HenPlus.msg().println(
355: "connection is closed; reconnect.");
356: connect();
357: --retries;
358: }
359: } catch (Exception e) { /* ign */
360: }
361:
362: while (retries > 0) {
363: try {
364: result = _conn.createStatement();
365: ++_statementCount;
366: break;
367: } catch (Throwable t) {
368: HenPlus.msg().println(
369: "connection failure. Try to reconnect.");
370: try {
371: connect();
372: } catch (Exception e) { /* ign */
373: }
374: }
375: --retries;
376: }
377: return result;
378: }
379:
380: /* ------- Session Properties ----------------------------------- */
381:
382: private class ReadOnlyProperty extends BooleanPropertyHolder {
383:
384: ReadOnlyProperty() {
385: super (false);
386: _propertyValue = "off"; // 'off' sounds better in this context.
387: }
388:
389: public void booleanPropertyChanged(boolean switchOn)
390: throws Exception {
391: /*
392: * readonly requires a closed transaction.
393: */
394: if (!switchOn) {
395: getConnection().rollback(); // save choice.
396: } else {
397: /* if we switched off and the user has not closed the current
398: * transaction, setting readonly will throw an exception
399: * and will notify the user about what to do.. */
400: }
401: getConnection().setReadOnly(switchOn);
402: if (getConnection().isReadOnly() != switchOn) {
403: throw new Exception(
404: "JDBC-Driver ignores request; transaction closed before ?");
405: }
406: }
407:
408: public String getDefaultValue() {
409: return "off";
410: }
411:
412: public String getShortDescription() {
413: return "Switches on read only mode for optimizations.";
414: }
415: }
416:
417: private class AutoCommitProperty extends BooleanPropertyHolder {
418:
419: AutoCommitProperty() {
420: super (false);
421: _propertyValue = "off"; // 'off' sounds better in this context.
422: }
423:
424: public void booleanPropertyChanged(boolean switchOn)
425: throws Exception {
426: /*
427: * due to a bug in Sybase, we have to close the
428: * transaction first before setting autcommit.
429: * This is probably a save choice to do, since the user asks
430: * for autocommit..
431: */
432: if (switchOn) {
433: getConnection().commit();
434: }
435: getConnection().setAutoCommit(switchOn);
436: if (getConnection().getAutoCommit() != switchOn) {
437: throw new Exception("JDBC-Driver ignores request");
438: }
439: }
440:
441: public String getDefaultValue() {
442: return "off";
443: }
444:
445: public String getShortDescription() {
446: return "Switches auto commit";
447: }
448: }
449:
450: private class IsolationLevelProperty extends
451: EnumeratedPropertyHolder {
452: private final Map _availableValues;
453: private final String _initialValue;
454:
455: IsolationLevelProperty(Map availableValues, int currentValue) {
456: super (availableValues.keySet());
457: _availableValues = availableValues;
458:
459: // sequential search .. doesn't matter, not much do do
460: String initValue = null;
461: Iterator it = availableValues.entrySet().iterator();
462: while (it.hasNext()) {
463: Map.Entry entry = (Map.Entry) it.next();
464: Integer isolationLevel = (Integer) entry.getValue();
465: if (isolationLevel.intValue() == currentValue) {
466: initValue = (String) entry.getKey();
467: break;
468: }
469: }
470: _propertyValue = _initialValue = initValue;
471: }
472:
473: public String getDefaultValue() {
474: return _initialValue;
475: }
476:
477: protected void enumeratedPropertyChanged(int index, String value)
478: throws Exception {
479: Integer isolationLevel = (Integer) _availableValues
480: .get(value);
481: if (isolationLevel == null) {
482: throw new IllegalArgumentException("invalid value");
483: }
484: int isolation = isolationLevel.intValue();
485: getConnection().setTransactionIsolation(isolation);
486: if (getConnection().getTransactionIsolation() != isolation) {
487: throw new Exception("JDBC-Driver ignores request");
488: }
489: }
490:
491: public String getShortDescription() {
492: return "sets the transaction isolation level";
493: }
494: }
495: }
496:
497: /*
498: * Local variables:
499: * c-basic-offset: 4
500: * compile-command: "ant -emacs -find build.xml"
501: * End:
502: */
|