001: /*
002: * The contents of this file are subject to the Mozilla Public License
003: * Version 1.1 (the "License"); you may not use this file except in
004: * compliance with the License. You may obtain a copy of the License at
005: * http://www.mozilla.org/MPL/
006: *
007: * Software distributed under the License is distributed on an "AS IS"
008: * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
009: * License for the specific language governing rights and limitations
010: * under the License.
011: *
012: * The Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
013: *
014: * The Initial Developer of the Original Code is iSQL-Viewer, A Mutli-Platform Database Tool.
015: * Portions created by Mark A. Kobold are Copyright (C) 2000-2007. All Rights Reserved.
016: *
017: * Contributor(s):
018: * Mark A. Kobold [mkobold <at> isqlviewer <dot> com].
019: *
020: * If you didn't download this code from the following link, you should check
021: * if you aren't using an obsolete version: http://www.isqlviewer.com
022: */
023: package org.isqlviewer.sql;
024:
025: import java.io.File;
026: import java.io.PrintWriter;
027: import java.security.AccessController;
028: import java.security.PrivilegedAction;
029: import java.sql.Connection;
030: import java.sql.Driver;
031: import java.sql.DriverManager;
032: import java.sql.SQLException;
033: import java.text.MessageFormat;
034: import java.util.ArrayList;
035: import java.util.Properties;
036:
037: import javax.sql.DataSource;
038:
039: import org.apache.log4j.FileAppender;
040: import org.apache.log4j.Logger;
041: import org.apache.log4j.PatternLayout;
042: import org.apache.log4j.Priority;
043: import org.isqlviewer.JdbcCommandLogger;
044: import org.isqlviewer.util.IsqlToolkit;
045: import org.isqlviewer.util.LocalMessages;
046: import org.isqlviewer.util.StringUtilities;
047:
048: /**
049: * Class that represents the connection to the database provider.
050: * <p>
051: *
052: * @author Mark A. Kobold <mkobold at isqlviewer dot com>
053: * @version 1.0
054: */
055: public final class JdbcService implements DataSource {
056:
057: // ResourceBundle identifier for providing natural language errors and other messages.
058: private static final String RESOURCE_BUNDLE = "org.isqlviewer.sql.ResourceBundle";
059: // JDBC environment variable for specifying the user-name to the connection.
060: private static final String JDBC_ENV_PRINCIPAL = "user";
061: // JDBC environment variable for specifying the user-password to the connection.
062: private static final String JDBC_ENV_PASSWORD = "password";
063:
064: // detection variable when the connection parameters are modified after a connection is made.
065: private transient int modCount = 0;
066:
067: // JDBC URL as the connect string the underlying connection.
068: private String url = null;
069: // Username for authenticating against the underlying connection.
070: private String principal = null;
071: // Password for the user-name when authenticating against the underlying connnection.
072: private String credentials = null;
073: // Fully qualified Java class name of the actual driver to use.
074: private String driverClass = null;
075: // User defined description for showing in tools to describe this connection.
076: private String description = null;
077: // Set of platform specific configuration parameters for tailoring a JDBC connection correctly.
078: private Properties environment = new Properties();
079: // Number of seconds that a connection is allowed to have when creating a connection.
080: private int loginTimeout = 0;
081: // Internal logging facility provided by log4j.
082: private Logger logger = IsqlToolkit.getApplicationLogger();
083: // User defined name for identifying this service.
084: private String name = null;
085: //
086: private ServiceType type = null;
087:
088: // profile settings that determine certain behaviors for this service.
089: private ConnectionProfile profile = null;
090:
091: private ConnectionWrapper cachedConnection = null;
092:
093: private JdbcCommandLogger commandLogger = null;
094:
095: // localization component for providing natural language errors and messages within this class.
096: private static final LocalMessages messages = new LocalMessages(
097: RESOURCE_BUNDLE, JdbcService.class);
098:
099: public JdbcService() {
100:
101: this (null);
102: }
103:
104: public JdbcService(JdbcCommandLogger commandLogger) {
105:
106: this .commandLogger = commandLogger;
107: }
108:
109: public Connection getConnection() throws SQLException {
110:
111: return getConnection(principal, credentials);
112: }
113:
114: public Connection getConnection(String username, String password)
115: throws SQLException {
116:
117: return createDriverConnection(username, password);
118: }
119:
120: public PrintWriter getLogWriter() throws SQLException {
121:
122: return null;
123: }
124:
125: public void setLogWriter(PrintWriter writer) throws SQLException {
126:
127: throw new SQLException();
128: }
129:
130: public synchronized void setLoginTimeout(int loginTimeout)
131: throws SQLException {
132:
133: this .loginTimeout = loginTimeout;
134: modCount++;
135: }
136:
137: public synchronized int getLoginTimeout() throws SQLException {
138:
139: return loginTimeout;
140: }
141:
142: /**
143: * Get the user credentials/password for this connection.
144: * <p>
145: *
146: * @return password for the user-name with this connection.
147: */
148: public synchronized String getCredentials() {
149:
150: return credentials;
151: }
152:
153: /**
154: * Sets the credentials/password for the user of this connection.
155: * <p>
156: *
157: * @param credentials userpassword for this connection.
158: */
159: public synchronized void setCredentials(String credentials) {
160:
161: this .credentials = credentials;
162: modCount++;
163: }
164:
165: /**
166: * Get the fully qualified driver class name for this connection.
167: * <p>
168: *
169: * @return Java class name of the driver connection to the database.
170: */
171: public synchronized String getDriverClass() {
172:
173: return driverClass;
174: }
175:
176: /**
177: * Gets the user-defined description for this service.
178: * <p>
179: * Purpose of the description is to allow a short text to describe this service as deemed by the user.
180: *
181: * @return user-defined description of this service.
182: */
183: public synchronized String getDescription() {
184:
185: return description;
186: }
187:
188: /**
189: * Sets the description for this service.
190: * <p>
191: * Intent is to allow the user to define a short text regarding the purpose of this message.
192: *
193: * @param description to set for this service.
194: */
195: public synchronized void setDescription(String description) {
196:
197: this .description = description;
198: modCount++;
199: }
200:
201: /**
202: * Sets the driver class for creating JDBC connections.
203: * <p>
204: *
205: * @param driverClass fully-qualified Java class name of the JDBC driver.
206: */
207: public synchronized void setDriverClass(String driverClass) {
208:
209: this .driverClass = driverClass;
210: modCount++;
211: }
212:
213: /**
214: * Gets the current user-name of this connection.
215: * <p>
216: *
217: * @return user-name that is used to authenticate this connection.
218: */
219: public synchronized String getPrincipal() {
220:
221: return principal;
222: }
223:
224: /**
225: * Set the user-name to authenticate this connection with.
226: * <p>
227: *
228: * @param principal the user-name to use when authenticating this connection.
229: */
230: public synchronized void setPrincipal(String principal) {
231:
232: this .principal = principal;
233: modCount++;
234: }
235:
236: /**
237: * Get the JDBC URL or connect string for this connection.
238: * <p>
239: *
240: * @return the JDBC URL for this connection.
241: */
242: public synchronized String getUrl() {
243:
244: return url;
245: }
246:
247: /**
248: * Set the JDBC URL or connect string for this connection.
249: * <p>
250: * Please refer to the JDBC Driver instructions for populating this field.
251: *
252: * @param url for the driver to use when creating a new connection.
253: */
254: public synchronized void setUrl(String url) {
255:
256: this .url = url;
257: modCount++;
258: }
259:
260: /**
261: * Get the connection environment for this connection.
262: * <p>
263: * This environment will contain the platform-specific configuration options for the JDBC driver to use when
264: * creating the connection.
265: * <p>
266: * Please refer to the JDBC Driver instructions for configuring driver properties.
267: *
268: * @return driver properties that can tune platform specific options for this connection.
269: */
270: public synchronized Properties getEnvironment() {
271:
272: return (Properties) environment.clone();
273: }
274:
275: /**
276: * @param environment The environment to set.
277: */
278: public synchronized void setEnvironment(Properties environment) {
279:
280: synchronized (this .environment) {
281: this .environment.clear();
282: this .environment.putAll(environment);
283: }
284: modCount++;
285: }
286:
287: /**
288: * Gets the connection profile used by this connection.
289: * <p>
290: *
291: * @return reference to the profile being used by this profile.
292: */
293: public synchronized ConnectionProfile getProfile() {
294:
295: return profile;
296: }
297:
298: /**
299: * Sets the profile for this service.
300: * <p>
301: *
302: * @param profile
303: */
304: public synchronized void setProfile(ConnectionProfile profile) {
305:
306: if (profile == null) {
307: throw new NullPointerException(
308: messages
309: .format("DataSource.cannot_use_null_connection_profile"));
310: }
311: this .profile = profile;
312: }
313:
314: /**
315: * Get the user-defined name for this service.
316: * <p>
317: * This name should identify this service to the end-user. This is the name that should be used when looking up this
318: * service initially.
319: *
320: * @return the conical name for this service.
321: */
322: public synchronized String getName() {
323:
324: return name;
325: }
326:
327: /**
328: * Sets the conical name for this service.
329: * <p>
330: *
331: * @param name for this service so that it can be accessed by name for the end-user.
332: */
333: public synchronized void setName(String name) {
334:
335: this .name = name;
336: modCount++;
337: }
338:
339: /**
340: * Sets the type of service this instance represents.
341: * <p>
342: * This parameter can be a bit confusing a iSQL-Viewer shared service is a service that is generally read-only and
343: * is loaded across the network from a shared location such as a intranet web server or even the local file system.
344: * <p>
345: * A private service is readable and writable by the user that created it, most cases the private services can also
346: * be a shared service for someone else depending on configurations.
347: *
348: * @param type the type of service to set this service as.
349: */
350: public void setType(ServiceType type) {
351:
352: if (type == null) {
353: throw new NullPointerException(ServiceType.class.getName());
354: }
355: this .type = type;
356: }
357:
358: /**
359: * Gets the type of service this instance represents.
360: * <p>
361: *
362: * @return the type of service this instance is.
363: */
364: public ServiceType getType() {
365:
366: return type;
367: }
368:
369: public Driver getDriver() {
370:
371: return AccessController
372: .doPrivileged(new PrivilegedAction<Driver>() {
373:
374: public Driver run() {
375:
376: ClassLoader classLoader = profile
377: .toClassLoader();
378: ClassLoader contextClassLoader = Thread
379: .currentThread()
380: .getContextClassLoader();
381: Thread.currentThread().setContextClassLoader(
382: classLoader);
383: try {
384: Class clazz = Class.forName(
385: getDriverClass(), true, profile
386: .toClassLoader());
387: return (Driver) clazz.newInstance();
388: } catch (Throwable t) {
389: // fall-back on the driver manager to get the connection.
390: error(messages
391: .format("DataSource.DriverError"),
392: t);
393: return null;
394: } finally {
395: Thread.currentThread()
396: .setContextClassLoader(
397: contextClassLoader);
398: }
399: }
400: });
401: }
402:
403: /**
404: * Logs informational error and supplementary message.
405: * <p>
406: * if informational messages are enabled the message and exception will be logged to designated destination.
407: *
408: * @param message informational message to log.
409: * @param error exception associated with the message.
410: */
411: void info(Object message, Throwable error) {
412:
413: if (logger.isInfoEnabled()) {
414: logger.info(message, error);
415: }
416: }
417:
418: /**
419: * Logs an informational message.
420: * <p>
421: * if informational loggins is enabled the message will be logged to designated destination.
422: *
423: * @param message informational message to log.
424: */
425: void info(Object message) {
426:
427: if (logger.isInfoEnabled()) {
428: logger.info(message);
429: }
430: }
431:
432: /**
433: * Logs debugging messages and supplementary exception.
434: * <p>
435: * if debug messages are enabled the message and exception will be logged to designated destination.
436: *
437: * @param message trace message to log.
438: * @param error exception associated with the message.
439: */
440: void trace(Object message, Throwable error) {
441:
442: if (logger.isDebugEnabled()) {
443: logger.debug(message, error);
444: }
445: }
446:
447: /**
448: * Logs message for debugging purposes.
449: * <p>
450: * if debugging is enabled the message will be logged to designated destination.
451: *
452: * @param message trace information to log.
453: */
454: void trace(Object message) {
455:
456: if (logger.isDebugEnabled()) {
457: logger.debug(message);
458: }
459: }
460:
461: /**
462: * Logs error messages and supplementary exception.
463: * <p>
464: * if error messages are enabled the message and exception will be logged to designated destination.
465: *
466: * @param message error message to log.
467: * @param error exception associated with the message.
468: */
469: void error(Object message, Throwable error) {
470:
471: logger.error(message, error);
472: }
473:
474: /**
475: * Logs an error message.
476: * <p>
477: * if error logging is enabled the message will be logged to designated destination.
478: *
479: * @param message error message to log.
480: */
481: void error(Object message) {
482:
483: logger.error(message);
484: }
485:
486: /**
487: * Logs warning messages and supplementary exception.
488: * <p>
489: *
490: * @param message warning message to log.
491: * @param error exception associated with the message.
492: */
493: void warn(Object message, Throwable error) {
494:
495: logger.warn(message, error);
496: }
497:
498: /**
499: * Logs warning messages.
500: * <p>
501: *
502: * @param message warning information to log.
503: */
504: void warn(Object message) {
505:
506: logger.warn(message);
507: }
508:
509: /**
510: * Build a connection to the database using the appropriate database driver.
511: * <p>
512: *
513: * @see DriverManager#getConnection(java.lang.String, java.util.Properties)
514: * @return a connection to the database
515: */
516: Connection createDriverConnection(final String username,
517: final String password) throws SQLException {
518:
519: if (cachedConnection != null) {
520: return cachedConnection;
521: }
522:
523: initializeLogging();
524: Object result = AccessController
525: .doPrivileged(new PrivilegedAction<Object>() {
526:
527: public Object run() {
528:
529: final String jdbcURL = getUrl();
530: final Properties env = (Properties) environment
531: .clone();
532: env.setProperty(JDBC_ENV_PRINCIPAL,
533: username == null ? "" : username);
534: env.setProperty(JDBC_ENV_PASSWORD,
535: password == null ? "" : password);
536: ClassLoader classLoader = profile
537: .toClassLoader();
538: ClassLoader contextClassLoader = Thread
539: .currentThread()
540: .getContextClassLoader();
541: Thread.currentThread().setContextClassLoader(
542: classLoader);
543: try {
544: Class clazz = Class.forName(
545: getDriverClass(), true, profile
546: .toClassLoader());
547: final ArrayList<Object> resultPlaceHolder = new ArrayList<Object>();
548: final Driver driver = (Driver) clazz
549: .newInstance();
550: long timeout = loginTimeout * 1000;
551: final Thread t = new Thread(new Runnable() {
552:
553: public void run() {
554:
555: try {
556: Connection connection = driver
557: .connect(jdbcURL, env);
558: if (connection != null) {
559: resultPlaceHolder
560: .add(connection);
561: } else {
562: throw new RuntimeException(
563: "Failed to connect to:'"
564: + jdbcURL
565: + "'");
566: }
567: } catch (SQLException sqle) {
568: error(
569: messages
570: .format("DataSource.driver_error"),
571: sqle);
572: resultPlaceHolder.add(sqle);
573: } catch (Throwable e) {
574: // fall-back on the driver manager to get the connection.
575: error(
576: messages
577: .format("DataSource.driver_error"),
578: e);
579: try {
580: resultPlaceHolder
581: .add(DriverManager
582: .getConnection(
583: jdbcURL,
584: env));
585: } catch (Throwable error) {
586: error(
587: messages
588: .format("DataSource.driver_error"),
589: error);
590: resultPlaceHolder
591: .add(error);
592: }
593: }
594: }
595: });
596:
597: t
598: .setName("privileged:iSQL-Viewer/createDriverConnection()");
599: t.start();
600: synchronized (t) {
601: t.join(timeout);
602: }
603:
604: if (!resultPlaceHolder.isEmpty()) {
605: return resultPlaceHolder.get(0);
606: }
607:
608: t.interrupt();
609: String time = StringUtilities
610: .getFullHumanReadableTime(timeout);
611: return new SQLException(messages.format(
612: "DataSource.connection_timed_out",
613: time));
614: } catch (InterruptedException ie) {
615: SQLException sqle = new SQLException(
616: messages
617: .format("DataSource.connection_interrupted"));
618: return sqle;
619: } catch (Throwable t) {
620: // fall-back on the driver manager to get the connection.
621: error(messages
622: .format("DataSource.DriverError"),
623: t);
624: try {
625: return DriverManager.getConnection(
626: jdbcURL, env);
627: } catch (Throwable error) {
628: error(
629: messages
630: .format("DataSource.DriverError"),
631: error);
632: return error;
633: }
634: } finally {
635: Thread.currentThread()
636: .setContextClassLoader(
637: contextClassLoader);
638: }
639: }
640: });
641:
642: if (result instanceof SQLException) {
643: throw (SQLException) result;
644: } else if (result instanceof Throwable) {
645: Throwable error = (Throwable) result;
646: throw new SQLException(error.getMessage());
647: } else if (result instanceof Connection) {
648: cachedConnection = new ConnectionWrapper(this ,
649: (Connection) result);
650: RegistrarFactory registrarFactory = RegistrarFactory
651: .getSharedInstance();
652: PlatformRegistrar registrar = registrarFactory
653: .lookupRegistrar(getDriverClass());
654: if (registrar != null) {
655: boolean registered = false;
656: try {
657: registered = registrar.register(cachedConnection
658: .getConnection());
659: if (registered) {
660: info(messages
661: .format(
662: "DataSource.platform_registration_sucessfull",
663: getName()));
664: } else {
665: warn(messages
666: .format(
667: "DataSource.failed_to_register_correctly",
668: getName()));
669: }
670: } catch (SQLException sqle) {
671: error(messages.format(
672: "DataSource.platform_registration_failed",
673: result), sqle);
674: }
675: }
676: return cachedConnection;
677: }
678: throw new RuntimeException(messages.format(
679: "DataSource.connectionerror", result));
680: }
681:
682: synchronized void clearConnection() {
683:
684: cachedConnection = null;
685: Logger newLogger = Logger.getLogger(getDriverClass());
686: newLogger.removeAllAppenders();
687: logger = IsqlToolkit.getApplicationLogger();
688: System.gc();
689: }
690:
691: /**
692: * Gets the current command logger for logging activity from this service.
693: * <p>
694: *
695: * @return <tt>null</tt> if no command logger is available.
696: */
697: public JdbcCommandLogger getCommandLogger() {
698:
699: return commandLogger;
700: }
701:
702: /**
703: * @param commandHistory
704: */
705: public void setCommandLogger(JdbcCommandLogger commandLogger) {
706:
707: this .commandLogger = commandLogger;
708: }
709:
710: private void initializeLogging() {
711:
712: // we will use the driver class package name instead of our own since the driver
713: // is likely to use a logger in this category if it also uses log4j
714: // TODO Add hook for java logging as well //
715: Object[] paths = driverClass.split("\\.");
716: String packageName = MessageFormat.format("{0}.{1}", paths);
717: Logger newLogger = Logger.getLogger(packageName);
718: newLogger.setAdditivity(true);
719: newLogger.removeAllAppenders();
720: PatternLayout patternLayout = new PatternLayout(
721: "%d{ISO8601} [%-5p] - {%t} - %m\n");
722: FileAppender fileAppender = new FileAppender();
723: fileAppender.setLayout(patternLayout);
724: File logFile = new File(IsqlToolkit.getLoggingDirectory(),
725: getName().concat(".log"));
726: fileAppender.setFile(logFile.getAbsolutePath());
727: fileAppender.setAppend(false);
728: // TODO make this go to DEBUG externally if user wants it too //
729: fileAppender.setThreshold(Priority.INFO);
730: fileAppender.activateOptions();
731: newLogger.addAppender(fileAppender);
732: logger = newLogger;
733: }
734: }
|