/* * Copyright (c) 2009, SQL Power Group Inc. * * This file is part of SQL Power Library. * * SQL Power Library is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * SQL Power Library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.sqlpower.sql; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLDecoder; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.Driver; import java.sql.SQLException; import java.util.Properties; import org.apache.log4j.Logger; import ca.sqlpower.sql.jdbcwrapper.ConnectionDecorator; public class JDBCDataSource extends SPDataSource { public static final Logger logger = Logger.getLogger(JDBCDataSource.class); /* * constants used as keys to get into the properties map. the shared * heritage of this class explains why some constants use the prefix DBCS_ * while others use the prefix PL_. */ public static final String DBCS_DRIVER_CLASS = "JDBC Driver Class"; public static final String DBCS_URL = "JDBC URL"; public static final String DBCS_JAR = "JAR File"; public static final String DBCS_CONNECTION_TYPE = "Connection Type"; public static final String PL_TYPE = "Type"; public static final String PL_DSN = "DSN"; public static final String PL_SCHEMA_OWNER = "PL Schema Owner"; public static final String PL_UID = "UID"; public static final String PL_PWD = "PWD"; public static final String PL_TNS = "TNS Name"; public static final String PL_DATABASE_NAME = "Database Name"; public static final String PL_IP = "IP"; public static final String PL_PORT = "PORT"; /** * The custom JDBC classloaders in this app support special "builtin:" and * "server:" filename prefixes, which means a JAR file as a resource on the * classpath or on a remote HTTP server rather than an absolute local path * name. This method will take a string that might be a builtin reference * and return the java.io.File object that points to the builtin file's real * location in the filesystem. If the given string doesn't start with * builtin: or server:, it will be treated as a normal (absolute or * relative) file pathname. * * @param jarFileName * The builtin: or server: file spec or a normal local path name * @param classLoader * The classloader against which to resolve the builtin resource * names. If no builtin: resources are expected, this may be * null. * @param serverBaseURI * The base URI against which to resolve server: resourcve names. * If no server: resources are expected, this may be null. * @return a URL object that refers to the given filespec. It is not tested * for existence, so trying to read it may cause an IO Exception. */ public static URL jarSpecToFile(String jarFileName, ClassLoader classLoader, URI serverBaseURI) { URL location; try { if (jarFileName.startsWith(BUILTIN)) { String jarName = jarFileName.substring(BUILTIN.length()); location = classLoader.getResource(jarName); if (location == null) { logger.warn("Couldn't find built-in system resource \"" + jarName + "\". Skipping it."); } } else if (jarFileName.startsWith(SPDataSource.SERVER)) { if (serverBaseURI == null) { throw new IllegalArgumentException( "The JDBC driver at " + jarFileName + " can't" + " be located because no server base URI was specified"); } String jarFilePath = jarFileName.substring(SPDataSource.SERVER .length()); // Need to decode the URI to a URL to convert escaped characters // to their real values, // ie spaces described as %20 will be replaced by actual spaces location = new URL(URLDecoder.decode(serverBaseURI.toString(), "UTF-8")); location = new URL(location, jarFilePath); } else if (jarFileName.startsWith("file:")) { // this spec has already been turned into a file: url location = new URI(jarFileName).toURL(); } else { location = new File(jarFileName).toURI().toURL(); } } catch (MalformedURLException e) { throw new RuntimeException(e); } catch (URISyntaxException e) { throw new RuntimeException(e); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } return location; } /** * Builds a string of the DB connection properties to display to the user * when testing a DB connection or in a log of a SQL query. <br> * NOTE: This method uses the StringBuilder class to create strings which is * not thread safe. * <p> * * @param ds * - The SPDataSource for which connection details are wanted * <p> * @param testConnection * - This boolean should be true when the string is to be * displayed when testing a connection. False, otherwise. The * boolean decides whether or not to display the datasource's * name. There is no point in displaying the datasource's name if * it is already shown to the user as it is when testing a * connection (in one of the text fields) * <p> * @return A string of the DB connection properties */ public static String getConnectionInfoString(JDBCDataSource ds, boolean testConnection) { // If the connection fails, it returns an empty string StringBuilder summary = new StringBuilder(""); Connection con = null; try { con = ds.createConnection(); DatabaseMetaData dbmd = con.getMetaData(); if (!testConnection) { summary.append(ds.getDisplayName() + ": \n"); } summary.append("Database Product Name: ").append( dbmd.getDatabaseProductName() + "\n"); summary.append("Database Product Version: ").append( dbmd.getDatabaseProductVersion() + "\n"); summary.append("Database Driver Name: ").append( dbmd.getDriverName() + "\n"); summary.append("Database Driver Version: ").append( dbmd.getDriverVersion() + "\n"); } catch (SQLException e) { logger.error("Couldn't get database metadata!", e); } finally { try { if (con != null) { con.close(); } } catch (SQLException ex) { logger.warn("Couldn't close connection", ex); } } return summary.toString(); } /** * A property change listener that should be connected to the current * parentType. Its function is to keep the parent type name in the property * map in sync with the parent type's actual name. */ private class ParentTypeNameSynchronizer implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent evt) { if (evt.getSource() == parentType && "name".equals(evt.getPropertyName())) { put(DBCS_CONNECTION_TYPE, (String) evt.getNewValue()); } } } private final ParentTypeNameSynchronizer parentTypeUpdater = new ParentTypeNameSynchronizer(); /** * This is the type (or class, but we don't mean Java class) for this data * source. It provides defaults for all connections to the same type of * database (for instance, all Oracle 8i data sources should have the same * parent type, which knows where to find the Oracle driver, and what an * Oracle JDBC THIN URL looks like). * <p> * Warning: If you modify this value directly (not using setParentType()) * then you have to remove the listener from the old parent type and add it * to the new one. */ private JDBCDataSourceType parentType; /** * For purposes of setting the dropdown box in the DBCS panel we have to * know whether a datasource type has a parent or if it is a root. Since we * always set the parent type as a default one, there is no way to use a * parentType == null check to determine if a parent is set. This flag is * set to true when setParent is called. */ private boolean parentSet = false; public JDBCDataSource(DataSourceCollection parentCollection) { super(parentCollection); setParentType(new JDBCDataSourceType( parentCollection.getServerBaseURI())); parentSet = false; } public JDBCDataSource(SPDataSource copy) { super(copy); if (copy instanceof JDBCDataSource) { setParentType(((JDBCDataSource) copy).parentType); } } /** * Creates a new connection to the database described by the properties of * this Data Source. Doesn't do any pooling, so if you want a pool of * connections then make it yourself (the Architect does this in * SQLDatabase.getConnection()). */ public Connection createConnection() throws SQLException { try { if (getParentType() == null) { throw new SQLException("Data Source \"" + getName() + "\" has no database type."); } getParentType().checkConnectPrereqs(); if (getUrl() == null || getUrl().trim().length() == 0) { throw new SQLException("Data Source \"" + getName() + "\" has no JDBC URL."); } // don't check for missing username -- this is allowed when using // SQL Server "integrated security" if (logger.isDebugEnabled()) { ClassLoader cl = this.getClass().getClassLoader(); StringBuffer loaders = new StringBuffer(); loaders.append("Local Classloader chain: "); while (cl != null) { loaders.append(cl).append(", "); cl = cl.getParent(); } logger.debug(loaders); } Driver driver = (Driver) Class.forName(getDriverClass(), true, getParentType().getJdbcClassLoader()).newInstance(); logger.info("Driver Class " + getDriverClass() + " loaded without exception"); if (!driver.acceptsURL(getUrl())) { throw new SQLException("Couldn't connect to database \"" + getName() + "\":\n" + "JDBC Driver " + getDriverClass() + "\n" + "does not accept the URL " + getUrl()); } Properties connectionProps = new Properties(); connectionProps.setProperty("user", getUser()); connectionProps.setProperty("password", getPass()); // XXX this platform-specific fix should be moved into the config // file // once we switch from pl.ini to an XML format and allow arbitrary // user-specified connection properties. if (getDriverClass().endsWith("OracleDriver")) { connectionProps.setProperty("remarksReporting", "true"); } Connection realConnection = driver.connect(getUrl(), connectionProps); if (realConnection == null) { throw new SQLException( "JDBC Driver returned a null connection!"); } Connection connection = ConnectionDecorator .createFacade(realConnection); logger.debug("Connection class is: " + connection.getClass().getName()); return connection; } catch (ClassNotFoundException e) { logger.warn("Driver Class not found", e); SQLException sqlException = new SQLException("JDBC Driver \"" + getDriverClass() + "\" not found."); sqlException.initCause(e); throw sqlException; } catch (InstantiationException e) { logger.error( "Creating SQL Exception to conform to interface. Real exception is: ", e); throw new SQLException("Couldn't create an instance of the " + "JDBC driver '" + getDriverClass() + "'. " + e.getMessage()); } catch (IllegalAccessException e) { logger.error( "Creating SQL Exception to conform to interface. Real exception is: ", e); throw new SQLException("Couldn't connect to database because the " + "JDBC driver has no public constructor (this is bad). " + e.getMessage()); } } public void copyFrom(JDBCDataSource dbcs) { super.copyFrom(dbcs); setParentType(dbcs.getParentType()); } // ------------------- accessors and mutators for the flat property stuff // ------------------------ /** * Gets the value of url * * @return the value of url */ public String getUrl() { return get(DBCS_URL); } /** * Sets the value of url * * @param argUrl * Value to assign to this.url */ public void setUrl(String argUrl) { putImpl(DBCS_URL, argUrl, "url"); } /** * Gets the value of driverClass * * @return the value of driverClass */ public String getDriverClass() { if (getParentType() == null) { return null; } else { return getParentType().getJdbcDriver(); } } /** * Gets the value of user * * @return the value of user */ public String getUser() { return get(PL_UID); } /** * Sets the value of user * * @param argUser * Value to assign to this.user */ public void setUser(String argUser) { putImpl(PL_UID, argUser, "user"); } /** * Gets the value of pass * * @return the value of pass */ public String getPass() { return get(PL_PWD); } /** * Sets the value of pass * * @param argPass * Value to assign to this.pass */ public void setPass(String argPass) { putImpl(PL_PWD, argPass, "pass"); } public String getPlSchema() { return get(PL_SCHEMA_OWNER); } public void setPlSchema(String schema) { putImpl(PL_SCHEMA_OWNER, schema, "plSchema"); } public String getPlDbType() { return get(PL_TYPE); } public void setPlDbType(String type) { putImpl(PL_TYPE, type, "plDbType"); } public String getOdbcDsn() { return get(PL_DSN); } public void setOdbcDsn(String dsn) { putImpl(PL_DSN, dsn, "odbcDsn"); } /** * Returns the parent type configured for this data source. */ public JDBCDataSourceType getParentType() { return parentType; } /** * Sets the parent type configured for this data source, and fires a * property change event if the new value differs from the existing one. */ public void setParentType(JDBCDataSourceType type) { if (parentType != null) { parentType.removePropertyChangeListener(parentTypeUpdater); } parentType = type; if (parentType != null) { parentType.addPropertyChangeListener(parentTypeUpdater); } parentSet = true; putImpl(DBCS_CONNECTION_TYPE, type.getName(), "parentType"); } public boolean isParentSet() { return parentSet; } /** * Prints some info from this data source. For use in debugging. */ public String toString() { return getDisplayName(); } }