// jTDS JDBC Driver for Microsoft SQL Server and Sybase // Copyright (C) 2004 The jTDS Project // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This 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 // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // package net.sourceforge.jtds.jdbc; import java.lang.ref.WeakReference; import java.sql.*; import java.net.UnknownHostException; import java.io.*; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.HashSet; import java.util.Random; import net.sourceforge.jtds.jdbc.cache.*; import net.sourceforge.jtds.util.*; /** * jTDS implementation of the java.sql.Connection interface. * <p> * Implementation notes: * <ol> * <li>Environment setting code carried over from old jTDS otherwise * generally a new implementation of Connection. * <li>Connection properties and SQLException text messages are loaded from * a properties file. * <li>Character set choices are also loaded from a resource file and the original * Encoder class has gone. * <li>Prepared SQL statements are converted to procedures in the prepareSQL method. * <li>Use of Stored procedures is optional and controlled via connection property. * <li>This Connection object maintains a table of weak references to associated * statements. This allows the connection object to control the statements (for * example to close them) but without preventing them being garbage collected in * a pooled environment. * </ol> * * @author Mike Hutchinson * @author Alin Sinpalean * @version $Id: JtdsConnection.java,v 1.119.2.14 2010-05-17 10:27:00 ickzon Exp $ */ public class JtdsConnection implements java.sql.Connection { /** * SQL query to determine the server charset on Sybase. */ private static final String SYBASE_SERVER_CHARSET_QUERY = "select name from master.dbo.syscharsets where id =" + " (select value from master.dbo.sysconfigures where config=131)"; /** * SQL query to determine the server charset on MS SQL Server 6.5. */ private static final String SQL_SERVER_65_CHARSET_QUERY = "select name from master.dbo.syscharsets where id =" + " (select csid from master.dbo.syscharsets, master.dbo.sysconfigures" + " where config=1123 and id = value)"; /** Sybase initial connection string. */ private static final String SYBASE_INITIAL_SQL = "SET TRANSACTION ISOLATION LEVEL 1\r\n" + "SET CHAINED OFF\r\n" + "SET QUOTED_IDENTIFIER ON\r\n"+ "SET TEXTSIZE 2147483647"; /** * SQL Server initial connection string. Also contains a * <code>SELECT @@MAX_PRECISION</code> query to retrieve * the maximum precision for DECIMAL/NUMERIC data. */ private static final String SQL_SERVER_INITIAL_SQL = "SELECT @@MAX_PRECISION\r\n" + "SET TRANSACTION ISOLATION LEVEL READ COMMITTED\r\n" + "SET IMPLICIT_TRANSACTIONS OFF\r\n" + "SET QUOTED_IDENTIFIER ON\r\n"+ "SET TEXTSIZE 2147483647"; /** * SQL Server custom transaction isolation level. */ public static final int TRANSACTION_SNAPSHOT = 4096; /* * Conection attributes */ /** The orginal connection URL. */ private final String url; /** The server host name. */ private String serverName; /** The server port number. */ private int portNumber; /** The make of SQL Server (sybase/microsoft). */ private int serverType; /** The SQL Server instance. */ private String instanceName; /** The requested database name. */ private String databaseName; /** The current database name. */ private String currentDatabase; /** The Windows Domain name. */ private String domainName; /** The database user ID. */ private String user; /** The user password. */ private String password; /** The server character set. */ private String serverCharset; /** The application name. */ private String appName; /** The program name. */ private String progName; /** Workstation ID. */ private String wsid; /** The server message language. */ private String language; /** The client MAC Address. */ private String macAddress; /** The server protocol version. */ private int tdsVersion; /** The network TCP/IP socket. */ private final SharedSocket socket; /** The cored TDS protocol object. */ private final TdsCore baseTds; /** The initial network packet size. */ private int netPacketSize = TdsCore.MIN_PKT_SIZE; /** User requested packet size. */ private int packetSize; /** SQL Server 2000 collation. */ private byte collation[]; /** True if user specifies an explicit charset. */ private boolean charsetSpecified; /** The database product name eg SQL SERVER. */ private String databaseProductName; /** The product version eg 11.92. */ private String databaseProductVersion; /** The major version number eg 11. */ private int databaseMajorVersion; /** The minor version number eg 92. */ private int databaseMinorVersion; /** True if this connection is closed. */ private volatile boolean closed; /** True if this connection is read only. */ private boolean readOnly; /** List of statements associated with this connection. */ private final ArrayList statements = new ArrayList(); /** Default transaction isolation level. */ private int transactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED; /** Default auto commit state. */ private volatile boolean autoCommit = true; /** Diagnostc messages for this connection. */ private final SQLDiagnostic messages; /** Connection's current rowcount limit. */ private int rowCount; /** Connection's current maximum field size limit. */ private int textSize; /** Maximum decimal precision. */ private int maxPrecision = TdsData.DEFAULT_PRECISION_38; // Sybase default /** Stored procedure unique ID number. */ private int spSequenceNo = 1; /** Cursor unique ID number. */ private int cursorSequenceNo = 1; /** Procedures in this transaction. */ private final ArrayList procInTran = new ArrayList(); /** Java charset for encoding. */ private CharsetInfo charsetInfo; /** Method for preparing SQL used in Prepared Statements. */ private int prepareSql; /** The amount of LOB data to buffer in memory. */ private long lobBuffer; /** The maximum number of statements to keep open. */ private int maxStatements; /** Statement cache.*/ private StatementCache statementCache; /** Send parameters as unicode. */ private boolean useUnicode = true; /** Use named pipe IPC instead of TCP/IP sockets. */ private boolean namedPipe; /** Only return the last update count. */ private boolean lastUpdateCount; /** TCP_NODELAY */ private boolean tcpNoDelay = true; /** Login timeout value in seconds or 0. */ private int loginTimeout; /** Sybase capability mask.*/ private int sybaseInfo; /** True if running distributed transaction. */ private boolean xaTransaction; /** Current emulated XA State eg start/end/prepare etc. */ private int xaState; /** Current XA Transaction ID. */ private Object xid; /** True if driver should emulate distributed transactions. */ private boolean xaEmulation = true; /** Mutual exclusion lock to control access to connection. */ private final Semaphore mutex = new Semaphore(1); /** Socket timeout value in seconds or 0. */ private int socketTimeout; /** True to enable socket keep alive. */ private boolean socketKeepAlive; /** The process ID to report to a server when connecting. */ private static Integer processId; /** SSL setting. */ private String ssl; /** The maximum size of a batch. */ private int batchSize; /** Use metadata cache for prepared statements. */ private boolean useMetadataCache; /** Use fast forward cursors for forward only result sets. */ private boolean useCursors; /** The directory to buffer data to */ private File bufferDir; /** The global buffer memory limit for all connections (in kilobytes). */ private int bufferMaxMemory; /** The minimum number of packets per statement to buffer to memory. */ private int bufferMinPackets; /** Map large types (IMAGE and TEXT/NTEXT) to LOBs by default. */ private boolean useLOBs; /** A cached <code>TdsCore</code> instance to reuse on new statements. */ private TdsCore cachedTds; /** The local address to bind to when connecting to a database via TCP/IP. */ private String bindAddress; /** Force use of jCIFS library on Windows when connecting via named pipes. */ private boolean useJCIFS; /** When doing NTLM authentication, send NTLMv2 response rather than regular response */ private boolean useNTLMv2 = false; /** Force Kerberos authentication */ private boolean useKerberos = false; /** the number of currently open connections */ private static int[] connections = new int[1]; /** The list of savepoints. */ private ArrayList savepoints; /** Maps each savepoint to a list of temp procedures created since the savepoint */ private Map savepointProcInTran; /** Counter for generating unique savepoint identifiers */ private int savepointId; /** * Default constructor. * <p/> * Used for testing. */ private JtdsConnection() { synchronized( connections ) { connections[0] ++; } url = null; socket = null; baseTds = null; messages = null; } /** * Create a new database connection. * * @param url The connection URL starting jdbc:jtds:. * @param info The additional connection properties. * @throws SQLException */ JtdsConnection(String url, Properties info) throws SQLException { synchronized( connections ) { connections[0] ++; } this.url = url; // // Extract properties into instance variables // unpackProperties(info); messages = new SQLDiagnostic(serverType); // // Get the instance port, if it is specified. // Named pipes use instance names differently. // if (instanceName.length() > 0 && !namedPipe) { try { final MSSqlServerInfo msInfo = new MSSqlServerInfo(serverName); portNumber = msInfo.getPortForInstance(instanceName); } catch (SQLException e) { // may already have a valid portNumber but Microsoft SQL Server Browser disabled if (portNumber <= 0) { throw e; } } if (portNumber == -1) { throw new SQLException( Messages.get("error.msinfo.badinst", serverName, instanceName), "08003"); } } SharedSocket.setMemoryBudget(bufferMaxMemory * 1024); SharedSocket.setMinMemPkts(bufferMinPackets); SQLWarning warn; Object timer = null; boolean loginError = false; try { if (loginTimeout > 0) { // Start a login timer timer = TimerThread.getInstance().setTimer(loginTimeout * 1000, new TimerThread.TimerListener() { public void timerExpired() { if (socket != null) { socket.forceClose(); } } }); } if (namedPipe) { // Use named pipe socket = createNamedPipe(this); } else { // Use plain TCP/IP socket socket = new SharedSocket(this); } if( macAddress.equals( DefaultProperties.MAC_ADDRESS ) ) { String mac = socket.getMAC(); macAddress = mac != null ? mac : macAddress; } if (timer != null && TimerThread.getInstance().hasExpired(timer)) { // If the timer has expired during the connection phase, close // the socket and throw an exception socket.forceClose(); throw new IOException("Login timed out"); } if ( charsetSpecified ) { loadCharset(serverCharset); } else { // Need a default charset to process login packets for TDS 4.2/5.0 // Will discover the actual serverCharset later loadCharset("iso_1"); serverCharset = ""; // But don't send charset name to server! } // // Create TDS protocol object // baseTds = new TdsCore(this, messages); // // Negotiate SSL connection if required // if (tdsVersion >= Driver.TDS80 && !namedPipe) { baseTds.negotiateSSL(instanceName, ssl); } // // Now try to login // baseTds.login(serverName, databaseName, user, password, domainName, serverCharset, appName, progName, wsid, language, macAddress, packetSize); // // Save any login warnings so that they will not be overwritten by // the internal configuration SQL statements e.g. setCatalog() etc. // warn = messages.warnings; // Update the tdsVersion with the value in baseTds. baseTds sets // the TDS version for the socket and there are no other objects // with cached TDS versions at this point. tdsVersion = baseTds.getTdsVersion(); if (tdsVersion < Driver.TDS70 && databaseName.length() > 0) { // Need to select the default database setCatalog(databaseName); } // If charset is still unknown and the collation is not set either, // determine the charset by querying (we're using Sybase or SQL Server // 6.5) if ((serverCharset == null || serverCharset.length() == 0) && collation == null) { loadCharset(determineServerCharset()); } // Initial database settings. // Sets: auto commit mode = true // transaction isolation = read committed. if (serverType == Driver.SYBASE) { baseTds.submitSQL(SYBASE_INITIAL_SQL); } else { // Also discover the maximum decimal precision: 28 (default) // or 38 for MS SQL Server 6.5/7, or 38 for 2000 and later. Statement stmt = this.createStatement(); ResultSet rs = stmt.executeQuery(SQL_SERVER_INITIAL_SQL); if (rs.next()) { maxPrecision = rs.getByte(1); } rs.close(); stmt.close(); } } catch (UnknownHostException e) { loginError = true; throw Support.linkException( new SQLException(Messages.get("error.connection.badhost", e.getMessage()), "08S03"), e); } catch (IOException e) { loginError = true; if (loginTimeout > 0 && e.getMessage().indexOf("timed out") >= 0) { throw Support.linkException( new SQLException(Messages.get("error.connection.timeout"), "HYT01"), e); } throw Support.linkException( new SQLException(Messages.get("error.connection.ioerror", e.getMessage()), "08S01"), e); } catch (SQLException e) { loginError = true; if (loginTimeout > 0 && e.getMessage().indexOf("socket closed") >= 0) { throw Support.linkException( new SQLException(Messages.get("error.connection.timeout"), "HYT01"), e); } throw e; } catch (RuntimeException e) { loginError = true; throw e; } finally { // fix for bug [1755448], socket not closed after login error if (loginError) { close(); } else if (timer != null) { // Cancel loginTimer TimerThread.getInstance().cancelTimer(timer); } } // // Restore any login warnings so that the user can retrieve them // by calling Connection.getWarnings() // messages.warnings = warn; } /** * Ensure all resources are released. */ protected void finalize() throws Throwable { try { close(); } finally { super.finalize(); } } /** * Creates a {@link SharedSocket} object representing a connection to a named * pipe. If the <code>os.name</code> system property starts with "Windows" * (case-insensitive) and the <code>useJCIFS</code> parameter is * <code>false</code>, a {@link SharedLocalNamedPipe} object is created. * Else a {@link SharedNamedPipe} is created which uses * <a href="http://jcifs.samba.org/">jCIFS</a> to provide a pure-Java * implementation of Windows named pipes. * <p> * This method will retry for <code>loginTimeout</code> seconds to create a * named pipe if an <code>IOException</code> continues to be thrown stating, * "All pipe instances are busy". If <code>loginTimeout</code> is set to * zero (e.g., not set), a default of 20 seconds will be used. * * @param connection the connection object * @return an object representing the named pipe connection * @throws IOException on error; if an <code>IOException</code> is thrown with * a message stating "All pipe instances are busy", then the method timed out * after <code>loginTimeout</code> milliseconds attempting to create a named pipe. */ private SharedSocket createNamedPipe(JtdsConnection connection) throws IOException { final long loginTimeout = connection.getLoginTimeout(); final long retryTimeout = (loginTimeout > 0 ? loginTimeout : 20) * 1000; final long startLoginTimeout = System.currentTimeMillis(); final Random random = new Random(startLoginTimeout); final boolean isWindowsOS = Support.isWindowsOS(); SharedSocket socket = null; IOException lastIOException = null; int exceptionCount = 0; do { try { if (isWindowsOS && !connection.getUseJCIFS()) { socket = new SharedLocalNamedPipe(connection); } else { socket = new SharedNamedPipe(connection); } } catch (IOException ioe) { exceptionCount++; lastIOException = ioe; if (ioe.getMessage().toLowerCase().indexOf("all pipe instances are busy") >= 0) { // Per a Microsoft knowledgebase article, wait 200 ms to 1 second each time // we get an "All pipe instances are busy" error. // http://support.microsoft.com/default.aspx?scid=KB;EN-US;165189 final int randomWait = random.nextInt(800) + 200; if (Logger.isActive()) { Logger.println("Retry #" + exceptionCount + " Wait " + randomWait + " ms: " + ioe.getMessage()); } try { Thread.sleep(randomWait); } catch (InterruptedException ie) { // Do nothing; retry again } } else { throw ioe; } } } while (socket == null && (System.currentTimeMillis() - startLoginTimeout) < retryTimeout); if (socket == null) { final IOException ioException = new IOException("Connection timed out to named pipe"); Support.linkException(ioException, lastIOException); throw ioException; } return socket; } /** * Retrive the shared socket. * * @return The <code>SharedSocket</code> object. */ SharedSocket getSocket() { return socket; } /** * Retrieve the TDS protocol version. * * @return The TDS version as an <code>int</code>. */ int getTdsVersion() { return tdsVersion; } /** * Retrieves the next unique stored procedure name. * <p>Notes: * <ol> * <li>Some versions of Sybase require an id with * a length of <= 10. * <li>The format of this name works for sybase and Microsoft * and allows for 16M names per session. * <li>The leading '#jtds' indicates this is a temporary procedure and * the '#' is removed by the lower level TDS5 routines. * </ol> * Not synchronized because it's only called from the synchronized * {@link #prepareSQL} method. * * @return the next temporary SP name as a <code>String</code> */ String getProcName() { String seq = "000000" + Integer.toHexString(spSequenceNo++).toUpperCase(); return "#jtds" + seq.substring(seq.length() - 6, seq.length()); } /** * Retrieves the next unique cursor name. * * @return the next cursor name as a <code>String</code> */ synchronized String getCursorName() { String seq = "000000" + Integer.toHexString(cursorSequenceNo++).toUpperCase(); return "_jtds" + seq.substring(seq.length() - 6, seq.length()); } /** * Try to convert the SQL statement into a statement prepare. * <p> * Synchronized because it accesses the procedure cache and the * <code>baseTds</code>, but the method call also needs to made in a * <code>synchronized (connection)</code> block together with the execution * (if the prepared statement is actually executed) to ensure the * transaction isn't rolled back between this method call and the actual * execution. * * @param pstmt the target prepared statement * @param sql the SQL statement to prepare * @param params the parameters * @param returnKeys indicates whether the statement will return * generated keys * @param cursorNeeded indicates whether a cursor prepare is needed * @return the SQL procedure name as a <code>String</code> or null if the * SQL cannot be prepared */ synchronized String prepareSQL(JtdsPreparedStatement pstmt, String sql, ParamInfo[] params, boolean returnKeys, boolean cursorNeeded) throws SQLException { if (prepareSql == TdsCore.UNPREPARED || prepareSql == TdsCore.EXECUTE_SQL) { return null; // User selected not to use procs } if (serverType == Driver.SYBASE) { if (tdsVersion != Driver.TDS50) { return null; // No longer support stored procs with 4.2 } if (returnKeys) { return null; // Sybase cannot use @@IDENTITY in proc } if (cursorNeeded) { // // We are going to use the CachedResultSet so there is // no point in preparing the SQL as it will be discarded // in favour of a version with "FOR BROWSE" appended. // return null; } } // // Check parameters set and obtain native types // for (int i = 0; i < params.length; i++) { if (!params[i].isSet) { throw new SQLException(Messages.get("error.prepare.paramnotset", Integer.toString(i+1)), "07000"); } TdsData.getNativeType(this, params[i]); if (serverType == Driver.SYBASE) { if ("text".equals(params[i].sqlType) || "image".equals(params[i].sqlType)) { return null; // Sybase does not support text/image params } } } String key = Support.getStatementKey(sql, params, serverType, getCatalog(), autoCommit, cursorNeeded); // // See if we have already built this one // ProcEntry proc = (ProcEntry) statementCache.get(key); if (proc != null) { // // Yes found in cache OK // // If already used by the statement, decrement use count if (pstmt.handles != null && pstmt.handles.contains(proc)) { proc.release(); } pstmt.setColMetaData(proc.getColMetaData()); if (serverType == Driver.SYBASE) { pstmt.setParamMetaData(proc.getParamMetaData()); } } else { // // No, so create the stored procedure now // proc = new ProcEntry(); if (serverType == Driver.SQLSERVER) { proc.setName( baseTds.microsoftPrepare( sql, params, cursorNeeded, pstmt.getResultSetType(), pstmt.getResultSetConcurrency())); if (proc.toString() == null) { proc.setType(ProcEntry.PREP_FAILED); } else if (prepareSql == TdsCore.TEMPORARY_STORED_PROCEDURES) { proc.setType(ProcEntry.PROCEDURE); } else { proc.setType((cursorNeeded) ? ProcEntry.CURSOR : ProcEntry.PREPARE); // Meta data may be returned by sp_prepare proc.setColMetaData(baseTds.getColumns()); pstmt.setColMetaData(proc.getColMetaData()); } // TODO Find some way of getting parameter meta data for MS } else { proc.setName(baseTds.sybasePrepare(sql, params)); if (proc.toString() == null) { proc.setType(ProcEntry.PREP_FAILED); } else { proc.setType(ProcEntry.PROCEDURE); } // Sybase gives us lots of useful information about the result set proc.setColMetaData(baseTds.getColumns()); proc.setParamMetaData(baseTds.getParameters()); pstmt.setColMetaData(proc.getColMetaData()); pstmt.setParamMetaData(proc.getParamMetaData()); } // OK we have built a proc so add it to the cache. addCachedProcedure(key, proc); } // Add the handle to the prepared statement so that the handles // can be used to clean up the statement cache properly when the // prepared statement is closed. if (pstmt.handles == null) { pstmt.handles = new HashSet(10); } pstmt.handles.add(proc); // Give the user the name will be null if prepare failed return proc.toString(); } /** * Add a stored procedure to the cache. * <p> * Not explicitly synchronized because it's only called by synchronized * methods. * * @param key The signature of the procedure to cache. * @param proc The stored procedure descriptor. */ void addCachedProcedure(String key, ProcEntry proc) { statementCache.put(key, proc); if (!autoCommit && proc.getType() == ProcEntry.PROCEDURE && serverType == Driver.SQLSERVER) { procInTran.add(key); } if (getServerType() == Driver.SQLSERVER && proc.getType() == ProcEntry.PROCEDURE) { // Only need to track SQL Server temp stored procs addCachedProcedure(key); } } /** * Remove a stored procedure from the cache. * <p> * Not explicitly synchronized because it's only called by synchronized * methods. * * @param key The signature of the procedure to remove from the cache. */ void removeCachedProcedure(String key) { statementCache.remove(key); if (!autoCommit) { procInTran.remove(key); } } /** * Retrieves the maximum statement cache size. * * @return the maximum statement cache size */ int getMaxStatements() { return maxStatements; } /** * Retrieves the server type. * * @return the server type as an <code>int</code> where 1 == SQLSERVER and * 2 == SYBASE. */ public int getServerType() { return serverType; } /** * Sets the network packet size. * * @param size the new packet size */ void setNetPacketSize(int size) { netPacketSize = size; } /** * Retrieves the network packet size. * * @return the packet size as an <code>int</code> */ int getNetPacketSize() { return netPacketSize; } /** * Retrieves the current row count on this connection. * * @return the row count as an <code>int</code> */ int getRowCount() { return rowCount; } /** * Sets the current row count on this connection. * * @param count the new row count */ void setRowCount(int count) { rowCount = count; } /** * Retrieves the current maximum textsize on this connection. * * @return the maximum textsize as an <code>int</code> */ public int getTextSize() { return textSize; } /** * Sets the current maximum textsize on this connection. * * @param textSize the new maximum textsize */ public void setTextSize(int textSize) { this.textSize = textSize; } /** * Retrieves the status of the lastUpdateCount flag. * * @return the lastUpdateCount flag as a <code>boolean</code> */ boolean getLastUpdateCount() { return lastUpdateCount; } /** * Retrieves the maximum decimal precision. * * @return the precision as an <code>int</code> */ int getMaxPrecision() { return maxPrecision; } /** * Retrieves the LOB buffer size. * * @return the LOB buffer size as a <code>long</code> */ long getLobBuffer() { return lobBuffer; } /** * Retrieves the Prepared SQL method. * * @return the Prepared SQL method */ int getPrepareSql() { return prepareSql; } /** * Retrieves the batch size to be used internally. * * @return the batch size as an <code>int</code> */ int getBatchSize() { return batchSize; } /** * Retrieves the boolean indicating whether metadata caching * is enabled. * * @return <code>true</code> if metadata caching is enabled, * <code>false</code> if caching is disabled */ boolean getUseMetadataCache() { return useMetadataCache; } /** * Indicates whether fast forward only cursors should be used for forward * only result sets. * * @return <code>true</code> if fast forward cursors are requested */ boolean getUseCursors() { return useCursors; } /** * Indicates whether large types (IMAGE and TEXT/NTEXT) should be mapped by * default to LOB types or <code>String</code> and <code>byte[]</code> * respectively. * * @return <code>true</code> if the default mapping should be to LOBs, * <code>false</code> otherwise */ boolean getUseLOBs() { return useLOBs; } /** * Indicates whether, when doing Windows authentication to an MS SQL server, * NTLMv2 should be used. When this is set to "false", LM and NTLM responses * are sent to the server, which should work fine in most cases. However, * some servers are configured to require LMv2 and NTLMv2. In these rare * cases, this property should be set to "true". */ boolean getUseNTLMv2() { return useNTLMv2; } /** * Return whether to use Kerberos authentication for MS SQL Server. */ boolean getUseKerberos() { return useKerberos; } /** * Retrieves the application name for this connection. * * @return the application name */ String getAppName() { return appName; } /** * Retrieves the bind address for this connection. * * @return the bind address */ String getBindAddress() { return bindAddress; } /** * Returns the directory where data should be buffered to. * * @return the directory where data should be buffered to. */ File getBufferDir() { return bufferDir; } /** * Retrieves the maximum amount of memory in Kb to buffer for <em>all</em> connections. * * @return the maximum amount of memory in Kb to buffer for <em>all</em> connections */ int getBufferMaxMemory() { return bufferMaxMemory; } /** * Retrieves the minimum number of packets to buffer per {@link Statement} for this connection. * * @return the minimum number of packets to buffer per {@link Statement} */ int getBufferMinPackets() { return bufferMinPackets; } /** * Retrieves the database name for this connection. * * @return the database name */ String getDatabaseName() { return databaseName; } /** * Retrieves the domain name for this connection. * * @return the domain name */ String getDomainName() { return domainName; } /** * Retrieves the instance name for this connection. * * @return the instance name */ String getInstanceName() { return instanceName; } /** * Retrieves the login timeout for this connection. * * @return the login timeout */ int getLoginTimeout() { return loginTimeout; } /** * Retrieves the socket timeout for this connection. * * @return the socket timeout */ int getSocketTimeout() { return socketTimeout; } /** * Retrieves whether to enable socket keep alive. * * @return <code>true</code> if the socket keep alive is enabled */ boolean getSocketKeepAlive() { return socketKeepAlive; } /** * Retrieves the process ID to send to a server when a connection is * established. * * @return the process ID */ int getProcessId() { return processId.intValue(); } /** * Retrieves the MAC (ethernet) address for this connection. * * @return the MAC (ethernet) address */ String getMacAddress() { return macAddress; } /** * Retrieves the named pipe setting for this connection. * * @return the named pipe setting */ boolean getNamedPipe() { return namedPipe; } /** * Retrieves the packet size for this connection. * * @return the packet size */ int getPacketSize() { return packetSize; } /** * Retrieves the password for this connection. * * @return the password */ String getPassword() { return password; } /** * Retrieves the port number for this connection. * * @return the port number */ int getPortNumber() { return portNumber; } /** * Retrieves the program name for this connection. * * @return the program name */ String getProgName() { return progName; } /** * Retrieves the server name for this connection. * * @return the server name */ String getServerName() { return serverName; } /** * Retrieves the tcpNoDelay setting for this connection. * * @return the tcpNoDelay setting */ boolean getTcpNoDelay() { return tcpNoDelay; } /** * Retrieves the useJCIFS setting for this connection. * * @return the useJCIFS setting */ boolean getUseJCIFS() { return useJCIFS; } /** * Retrieves the user for this connection. * * @return the user */ String getUser() { return user; } /** * Retrieves the workstation ID (WSID) for this connection. * * @return the workstation ID (WSID) */ String getWsid() { return wsid; } /** * Transfers the properties to the local instance variables. * * @param info The connection properties Object. * @throws SQLException If an invalid property value is found. */ protected void unpackProperties(Properties info) throws SQLException { serverName = info.getProperty(Messages.get(Driver.SERVERNAME)); portNumber = parseIntegerProperty(info, Driver.PORTNUMBER); serverType = parseIntegerProperty(info, Driver.SERVERTYPE); databaseName = info.getProperty(Messages.get(Driver.DATABASENAME)); instanceName = info.getProperty(Messages.get(Driver.INSTANCE)); domainName = info.getProperty(Messages.get(Driver.DOMAIN)); user = info.getProperty(Messages.get(Driver.USER)); password = info.getProperty(Messages.get(Driver.PASSWORD)); macAddress = info.getProperty(Messages.get(Driver.MACADDRESS)); appName = info.getProperty(Messages.get(Driver.APPNAME)); progName = info.getProperty(Messages.get(Driver.PROGNAME)); wsid = info.getProperty(Messages.get(Driver.WSID)); serverCharset = info.getProperty(Messages.get(Driver.CHARSET)); language = info.getProperty(Messages.get(Driver.LANGUAGE)); bindAddress = info.getProperty(Messages.get(Driver.BINDADDRESS)); lastUpdateCount = parseBooleanProperty(info,Driver.LASTUPDATECOUNT); useUnicode = parseBooleanProperty(info,Driver.SENDSTRINGPARAMETERSASUNICODE); namedPipe = parseBooleanProperty(info,Driver.NAMEDPIPE); tcpNoDelay = parseBooleanProperty(info,Driver.TCPNODELAY); useCursors = (serverType == Driver.SQLSERVER) && parseBooleanProperty(info,Driver.USECURSORS); useLOBs = parseBooleanProperty(info,Driver.USELOBS); useMetadataCache = parseBooleanProperty(info,Driver.CACHEMETA); xaEmulation = parseBooleanProperty(info,Driver.XAEMULATION); useJCIFS = parseBooleanProperty(info,Driver.USEJCIFS); charsetSpecified = serverCharset.length() > 0; useNTLMv2 = parseBooleanProperty(info,Driver.USENTLMV2); useKerberos = parseBooleanProperty(info,Driver.USEKERBEROS); //note:mdb in certain cases (e.g. NTLMv2) the domain name must be // all upper case for things to work. if( domainName != null ) domainName = domainName.toUpperCase(); Integer parsedTdsVersion = DefaultProperties.getTdsVersion(info.getProperty(Messages.get(Driver.TDS))); if (parsedTdsVersion == null) { throw new SQLException(Messages.get("error.connection.badprop", Messages.get(Driver.TDS)), "08001"); } tdsVersion = parsedTdsVersion.intValue(); packetSize = parseIntegerProperty(info, Driver.PACKETSIZE); if (packetSize < TdsCore.MIN_PKT_SIZE) { if (tdsVersion >= Driver.TDS70) { // Default of 0 means let the server specify packet size packetSize = (packetSize == 0) ? 0 : TdsCore.DEFAULT_MIN_PKT_SIZE_TDS70; } else if (tdsVersion == Driver.TDS42) { // Sensible minimum for older versions of TDS packetSize = TdsCore.MIN_PKT_SIZE; } // else for TDS 5 can auto negotiate } if (packetSize > TdsCore.MAX_PKT_SIZE) { packetSize = TdsCore.MAX_PKT_SIZE; } packetSize = (packetSize / 512) * 512; loginTimeout = parseIntegerProperty(info, Driver.LOGINTIMEOUT); socketTimeout = parseIntegerProperty(info, Driver.SOTIMEOUT); socketKeepAlive = parseBooleanProperty(info,Driver.SOKEEPALIVE); autoCommit = parseBooleanProperty(info,Driver.AUTOCOMMIT); String pid = info.getProperty(Messages.get(Driver.PROCESSID)); if ("compute".equals(pid)) { // only determine a single PID for the VM's (or classloader's) life time if (processId == null) { // random number until the real process ID can be determined processId = new Integer(new Random(System.currentTimeMillis()).nextInt(32768)); } } else if (pid.length() > 0) { processId = new Integer(parseIntegerProperty(info, Driver.PROCESSID)); } lobBuffer = parseLongProperty(info, Driver.LOBBUFFER); maxStatements = parseIntegerProperty(info, Driver.MAXSTATEMENTS); statementCache = new ProcedureCache(maxStatements); prepareSql = parseIntegerProperty(info, Driver.PREPARESQL); if (prepareSql < 0) { prepareSql = 0; } else if (prepareSql > 3) { prepareSql = 3; } // For Sybase use equivalent of sp_executesql. if (tdsVersion < Driver.TDS70 && prepareSql == TdsCore.PREPARE) { prepareSql = TdsCore.EXECUTE_SQL; } // For SQL 6.5 sp_executesql not available so use stored procedures. if (tdsVersion < Driver.TDS50 && prepareSql == TdsCore.EXECUTE_SQL) { prepareSql = TdsCore.TEMPORARY_STORED_PROCEDURES; } ssl = info.getProperty(Messages.get(Driver.SSL)); batchSize = parseIntegerProperty(info, Driver.BATCHSIZE); if (batchSize < 0) { throw new SQLException(Messages.get("error.connection.badprop", Messages.get(Driver.BATCHSIZE)), "08001"); } bufferDir = new File(info.getProperty(Messages.get(Driver.BUFFERDIR))); if (!bufferDir.isDirectory()) { if (!bufferDir.mkdirs()) { throw new SQLException(Messages.get("error.connection.badprop", Messages.get(Driver.BUFFERDIR)), "08001"); } } bufferMaxMemory = parseIntegerProperty(info, Driver.BUFFERMAXMEMORY); if (bufferMaxMemory < 0) { throw new SQLException(Messages.get("error.connection.badprop", Messages.get(Driver.BUFFERMAXMEMORY)), "08001"); } bufferMinPackets = parseIntegerProperty(info, Driver.BUFFERMINPACKETS); if (bufferMinPackets < 1) { throw new SQLException(Messages.get("error.connection.badprop", Messages.get(Driver.BUFFERMINPACKETS)), "08001"); } } /** * Parse a string property value into an boolean value. * * @param info The connection properties object. * @param key The message key used to retrieve the property name. * @return The boolean value of the string property value. * @throws SQLException If the property value can't be parsed. */ private static boolean parseBooleanProperty(final Properties info, final String key) throws SQLException { final String propertyName = Messages.get(key); String prop = info.getProperty(propertyName); if (! (prop == null || "true".equalsIgnoreCase(prop) || "false".equalsIgnoreCase(prop))) throw new SQLException( Messages.get("error.connection.badprop", propertyName), "08001"); return "true".equalsIgnoreCase(prop); } /** * Parse a string property value into an integer value. * * @param info The connection properties object. * @param key The message key used to retrieve the property name. * @return The integer value of the string property value. * @throws SQLException If the property value can't be parsed. */ private static int parseIntegerProperty(final Properties info, final String key) throws SQLException { final String propertyName = Messages.get(key); try { return Integer.parseInt(info.getProperty(propertyName)); } catch (NumberFormatException e) { throw new SQLException( Messages.get("error.connection.badprop", propertyName), "08001"); } } /** * Parse a string property value into a long value. * * @param info The connection properties object. * @param key The message key used to retrieve the property name. * @return The long value of the string property value. * @throws SQLException If the property value can't be parsed. */ private static long parseLongProperty(final Properties info, final String key) throws SQLException { final String propertyName = Messages.get(key); try { return Long.parseLong(info.getProperty(propertyName)); } catch (NumberFormatException e) { throw new SQLException( Messages.get("error.connection.badprop", propertyName), "08001"); } } /** * Retrieve the Java charset to use for encoding. * * @return the Charset name as a <code>String</code> */ protected String getCharset() { return charsetInfo.getCharset(); } /** * Retrieve the multibyte status of the current character set. * * @return <code>boolean</code> true if a multi byte character set */ protected boolean isWideChar() { return charsetInfo.isWideChars(); } /** * Retrieve the <code>CharsetInfo</code> instance used by this connection. * * @return the default <code>CharsetInfo</code> for this connection */ protected CharsetInfo getCharsetInfo() { return charsetInfo; } /** * Retrieve the sendParametersAsUnicode flag. * * @return <code>boolean</code> true if parameters should be sent as unicode. */ protected boolean getUseUnicode() { return useUnicode; } /** * Retrieve the Sybase capability data. * * @return Capability bit mask as an <code>int</code>. */ protected boolean getSybaseInfo(int flag) { return (sybaseInfo & flag) != 0; } /** * Set the Sybase capability data. * * @param mask The capability bit mask. */ protected void setSybaseInfo(int mask) { sybaseInfo = mask; } /** * Called by the protocol to change the current character set. * * @param charset the server character set name */ protected void setServerCharset(final String charset) throws SQLException { // If the user specified a charset, ignore environment changes if (charsetSpecified) { Logger.println("Server charset " + charset + ". Ignoring as user requested " + serverCharset + '.'); return; } if (!charset.equals(serverCharset)) { loadCharset(charset); if (Logger.isActive()) { Logger.println("Set charset to " + serverCharset + '/' + charsetInfo); } } } /** * Load the Java charset to match the server character set. * * @param charset the server character set */ private void loadCharset(String charset) throws SQLException { // MS SQL Server's iso_1 is Cp1252 not ISO-8859-1! if (getServerType() == Driver.SQLSERVER && charset.equalsIgnoreCase("iso_1")) { charset = "Cp1252"; } // Do not default to any charset; if the charset is not found we want // to know about it CharsetInfo tmp = CharsetInfo.getCharset(charset); if (tmp == null) { throw new SQLException( Messages.get("error.charset.nomapping", charset), "2C000"); } loadCharset(tmp, charset); serverCharset = charset; } /** * Load the Java charset to match the server character set. * * @param ci the <code>CharsetInfo</code> to load */ private void loadCharset(CharsetInfo ci, String ref) throws SQLException { try { "This is a test".getBytes(ci.getCharset()); charsetInfo = ci; } catch (UnsupportedEncodingException ex) { throw new SQLException( Messages.get("error.charset.invalid", ref, ci.getCharset()), "2C000"); } socket.setCharsetInfo(charsetInfo); } /** * Discovers the server charset for server versions that do not send * <code>ENVCHANGE</code> packets on login ack, by executing a DB * vendor/version specific query. * <p> * Will throw an <code>SQLException</code> if used on SQL Server 7.0 or * 2000; the idea is that the charset should already be determined from * <code>ENVCHANGE</code> packets for these DB servers. * <p> * Should only be called from the constructor. * * @return the default server charset * @throws SQLException if an error condition occurs */ private String determineServerCharset() throws SQLException { String queryStr = null; switch (serverType) { case Driver.SQLSERVER: if (databaseProductVersion.indexOf("6.5") >= 0) { queryStr = SQL_SERVER_65_CHARSET_QUERY; } else { // This will never happen. Versions 7.0 and 2000 of SQL // Server always send ENVCHANGE packets, even over TDS 4.2. throw new SQLException( "Please use TDS protocol version 7.0 or higher"); } break; case Driver.SYBASE: // There's no need to check for versions here queryStr = SYBASE_SERVER_CHARSET_QUERY; break; } Statement stmt = this.createStatement(); ResultSet rs = stmt.executeQuery(queryStr); rs.next(); String charset = rs.getString(1); rs.close(); stmt.close(); return charset; } /** * Set the default collation for this connection. * <p> * Set by a SQL Server 2000 environment change packet. The collation * consists of the following fields: * <ul> * <li>bits 0-19 - The locale eg 0x0409 for US English which maps to code * page 1252 (Latin1_General). * <li>bits 20-31 - Reserved. * <li>bits 32-39 - Sort order (csid from syscharsets) * </ul> * If the sort order is non-zero it determines the character set, otherwise * the character set is determined by the locale id. * * @param collation The new collation. */ void setCollation(byte[] collation) throws SQLException { String strCollation = "0x" + Support.toHex(collation); // If the user specified a charset, ignore environment changes if (charsetSpecified) { Logger.println("Server collation " + strCollation + ". Ignoring as user requested " + serverCharset + '.'); return; } CharsetInfo tmp = CharsetInfo.getCharset(collation); loadCharset(tmp, strCollation); this.collation = collation; if (Logger.isActive()) { Logger.println("Set collation to " + strCollation + '/' + charsetInfo); } } /** * Retrieve the SQL Server 2000 default collation. * * @return The collation as a <code>byte[5]</code>. */ byte[] getCollation() { return collation; } /** * Retrieves whether a specific charset was requested on creation. If this * is the case, all character data should be encoded/decoded using that * charset. */ boolean isCharsetSpecified() { return charsetSpecified; } /** * Called by the protcol to change the current database context. * * @param newDb The new database selected on the server. * @param oldDb The old database as known by the server. * @throws SQLException */ protected void setDatabase(final String newDb, final String oldDb) throws SQLException { if (currentDatabase != null && !oldDb.equalsIgnoreCase(currentDatabase)) { throw new SQLException(Messages.get("error.connection.dbmismatch", oldDb, databaseName), "HY096"); } currentDatabase = newDb; if (Logger.isActive()) { Logger.println("Changed database from " + oldDb + " to " + newDb); } } /** * Update the connection instance with information about the server. * * @param databaseProductName The server name eg SQL Server. * @param databaseMajorVersion The major version eg 11 * @param databaseMinorVersion The minor version eg 92 * @param buildNumber The server build number. */ protected void setDBServerInfo(String databaseProductName, int databaseMajorVersion, int databaseMinorVersion, int buildNumber) { this.databaseProductName = databaseProductName; this.databaseMajorVersion = databaseMajorVersion; this.databaseMinorVersion = databaseMinorVersion; if (tdsVersion >= Driver.TDS70) { StringBuilder buf = new StringBuilder(10); if (databaseMajorVersion < 10) { buf.append('0'); } buf.append(databaseMajorVersion).append('.'); if (databaseMinorVersion < 10) { buf.append('0'); } buf.append(databaseMinorVersion).append('.'); buf.append(buildNumber); while (buf.length() < 10) { buf.insert(6, '0'); } databaseProductVersion = buf.toString(); } else { databaseProductVersion = databaseMajorVersion + "." + databaseMinorVersion; } } /** * Removes a statement object from the list maintained by the connection * and cleans up the statement cache if necessary. * <p> * Synchronized because it accesses the statement list, the statement cache * and the <code>baseTds</code>. * * @param statement the statement to remove */ synchronized void removeStatement(JtdsStatement statement) throws SQLException { // Remove the JtdsStatement from the statement list synchronized (statements) { for (int i = 0; i < statements.size(); i++) { WeakReference wr = (WeakReference) statements.get(i); if (wr != null) { Statement stmt = (Statement) wr.get(); // Remove the statement if found but also remove all // statements that have already been garbage collected if (stmt == null || stmt == statement) { statements.set(i, null); } } } } if (statement instanceof JtdsPreparedStatement) { // Clean up the prepared statement cache; getObsoleteHandles will // decrement the usage count for the set of used handles Collection handles = statementCache.getObsoleteHandles( ((JtdsPreparedStatement) statement).handles); if (handles != null) { if (serverType == Driver.SQLSERVER) { // SQL Server unprepare StringBuilder cleanupSql = new StringBuilder(handles.size() * 32); for (Iterator iterator = handles.iterator(); iterator.hasNext(); ) { ProcEntry pe = (ProcEntry) iterator.next(); // Could get put back if in a transaction that is // rolled back pe.appendDropSQL(cleanupSql); } if (cleanupSql.length() > 0) { baseTds.executeSQL(cleanupSql.toString(), null, null, true, 0, -1, -1, true); baseTds.clearResponseQueue(); } } else { // Sybase unprepare for (Iterator iterator = handles.iterator(); iterator.hasNext(); ) { ProcEntry pe = (ProcEntry)iterator.next(); if (pe.toString() != null) { // Remove the Sybase light weight proc baseTds.sybaseUnPrepare(pe.toString()); } } } } } } /** * Adds a statement object to the list maintained by the connection. * <p/> * WeakReferences are used so that statements can still be closed and * garbage collected even if not explicitly closed by the connection. * * @param statement statement to add */ void addStatement(JtdsStatement statement) { synchronized (statements) { for (int i = 0; i < statements.size(); i++) { WeakReference wr = (WeakReference) statements.get(i); // FIXME: entries from statements should be dropped immediately // on GC, instead of being kept until overwritten or connection // being closed if (wr == null || wr.get() == null) { statements.set(i, new WeakReference(statement)); return; } } statements.add(new WeakReference(statement)); } } /** * Checks that the connection is still open. * * @throws SQLException if the connection is closed */ void checkOpen() throws SQLException { if (closed) { throw new SQLException( Messages.get("error.generic.closed", "Connection"), "HY010"); } } /** * Checks that this connection is in local transaction mode. * * @param method the method name being tested * @throws SQLException if in XA distributed transaction mode */ void checkLocal(String method) throws SQLException { if (xaTransaction) { throw new SQLException( Messages.get("error.connection.badxaop", method), "HY010"); } } /** * Reports that user tried to call a method which has not been implemented. * * @param method the method name to report in the error message * @throws SQLException always, with the not implemented message */ static void notImplemented(String method) throws SQLException { throw new SQLException( Messages.get("error.generic.notimp", method), "HYC00"); } /** * Retrieves the DBMS major version. * * @return the version as an <code>int</code> */ public int getDatabaseMajorVersion() { return databaseMajorVersion; } /** * Retrieves the DBMS minor version. * * @return the version as an <code>int</code> */ public int getDatabaseMinorVersion() { return databaseMinorVersion; } /** * Retrieves the DBMS product name. * * @return the name as a <code>String</code> */ String getDatabaseProductName() { return databaseProductName; } /** * Retrieves the DBMS product version. * * @return the version as a <code>String</code> */ String getDatabaseProductVersion() { return databaseProductVersion; } /** * Retrieves the original connection URL. * * @return the connection url as a <code>String</code> */ String getURL() { return url; } /** * Retrieves the host and port for this connection. * <p> * Used to identify same resource manager in XA transactions. * * @return the hostname and port as a <code>String</code> */ public String getRmHost() { return serverName + ':' + portNumber; } /** * Forces the closed status on the statement if an I/O error has occurred. */ void setClosed() { if (!closed) { closed = true; // Make sure we release the socket and all data buffered at the socket // level try { socket.close(); } catch (IOException e) { // Ignore; shouldn't happen anyway } } } /** * Invokes the <code>xp_jtdsxa</code> extended stored procedure on the * server. * <p/> * Synchronized because it accesses the <code>baseTds</code>. * * @param args the arguments eg cmd, rmid, flags etc. * @param data option byte data eg open string xid etc. * @return optional byte data eg OLE cookie * @throws SQLException if an error condition occurs */ synchronized byte[][] sendXaPacket(int args[], byte[] data) throws SQLException { ParamInfo params[] = new ParamInfo[6]; params[0] = new ParamInfo(Types.INTEGER, null, ParamInfo.RETVAL); params[1] = new ParamInfo(Types.INTEGER, new Integer(args[1]), ParamInfo.INPUT); params[2] = new ParamInfo(Types.INTEGER, new Integer(args[2]), ParamInfo.INPUT); params[3] = new ParamInfo(Types.INTEGER, new Integer(args[3]), ParamInfo.INPUT); params[4] = new ParamInfo(Types.INTEGER, new Integer(args[4]), ParamInfo.INPUT); params[5] = new ParamInfo(Types.VARBINARY, data, ParamInfo.OUTPUT); // // Execute our extended stored procedure (let's hope it is installed!). // baseTds.executeSQL(null, "master..xp_jtdsxa", params, false, 0, -1, -1, true); // // Now process results // ArrayList xids = new ArrayList(); while (!baseTds.isEndOfResponse()) { if (baseTds.getMoreResults()) { // This had better be the results from a xa_recover command while (baseTds.getNextRow()) { Object row[] = baseTds.getRowData(); if (row.length == 1 && row[0] instanceof byte[]) { xids.add(row[0]); } } } } messages.checkErrors(); if (params[0].getOutValue() instanceof Integer) { // Should be return code from XA command args[0] = ((Integer)params[0].getOutValue()).intValue(); } else { args[0] = -7; // XAException.XAER_RMFAIL } if (xids.size() > 0) { // List of XIDs from xa_recover byte list[][] = new byte[xids.size()][]; for (int i = 0; i < xids.size(); i++) { list[i] = (byte[])xids.get(i); } return list; } else if (params[5].getOutValue() instanceof byte[]) { // xa_open the xa connection ID // xa_start OLE Transaction cookie byte cookie[][] = new byte[1][]; cookie[0] = (byte[])params[5].getOutValue(); return cookie; } else { // All other cases return null; } } /** * Enlists the current connection in a distributed transaction. * * @param oleTranID the OLE transaction cookie or null to delist * @throws SQLException if an error condition occurs */ synchronized void enlistConnection(byte[] oleTranID) throws SQLException { if (oleTranID != null) { // TODO: Stored procs are no good but maybe prepare will be OK. prepareSql = TdsCore.EXECUTE_SQL; baseTds.enlistConnection(1, oleTranID); xaTransaction = true; } else { baseTds.enlistConnection(1, null); xaTransaction = false; } } /** * Sets the XA transaction ID when running in emulation mode. * * @param xid the XA Transaction ID */ void setXid(Object xid) { this.xid = xid; xaTransaction = xid != null; } /** * Gets the XA transaction ID when running in emulation mode. * * @return the transaction ID as an <code>Object</code> */ Object getXid() { return xid; } /** * Sets the XA state variable. * * @param value the XA state value */ void setXaState(int value) { xaState = value; } /** * Retrieves the XA state variable. * * @return the xa state variable as an <code>int</code> */ int getXaState() { return xaState; } /** * Retrieves the XA Emulation flag. * @return True if in XA emulation mode. */ boolean isXaEmulation() { return xaEmulation; } /** * Retrieves the connection mutex and acquires an exclusive lock on the * network connection. * * @return * the mutex object as a <code>Semaphore</code> */ Semaphore getMutex() { boolean interrupted = false; while( true ) { // JDBC can not be interrupted, retry on InterruptedException try { mutex.acquire(); break; } catch( InterruptedException e ) { // interrupt status is cleared now interrupted = true; } } // Bug [1596743] do not absorb interrupt status if( interrupted ) { Thread.currentThread().interrupt(); } return mutex; } /** * Releases (either closes or caches) a <code>TdsCore</code>. * * @param tds * the <code>TdsCore</code> instance to release * * @throws SQLException * if an error occurs while closing or cleaning up */ synchronized void releaseTds( TdsCore tds ) throws SQLException { if( cachedTds != null ) { // There's already a cached TdsCore; close this one tds.close(); } else { // No cached TdsCore; clean up this one and cache it tds.clearResponseQueue(); tds.cleanUp(); cachedTds = tds; } } /** * Retrieves the cached <code>TdsCore</code> or <code>null</code> if * nothing is cached and resets the cache (sets it to <code>null</code>). * * @return the value of {@link #cachedTds} * @todo Should probably synchronize on another object */ synchronized TdsCore getCachedTds() { TdsCore result = cachedTds; cachedTds = null; return result; } // // ------------------- java.sql.Connection interface methods ------------------- // public int getHoldability() throws SQLException { checkOpen(); return JtdsResultSet.HOLD_CURSORS_OVER_COMMIT; } synchronized public int getTransactionIsolation() throws SQLException { checkOpen(); return transactionIsolation; } synchronized public void clearWarnings() throws SQLException { checkOpen(); messages.clearWarnings(); } /** * Releases this <code>Connection</code> object's database and JDBC * resources immediately instead of waiting for them to be automatically * released. * <p> * Calling the method close on a <code>Connection</code> object that is * already closed is a no-op. * <p> * <b>Note:</b> A <code>Connection</code> object is automatically closed * when it is garbage collected. Certain fatal errors also close a * <code>Connection</code> object. * <p> * Synchronized because it accesses the statement list and the * <code>baseTds</code>. * * @throws SQLException if a database access error occurs */ synchronized public void close() throws SQLException { if (!closed) { try { // // Close any open statements // ArrayList tmpList; synchronized (statements) { tmpList = new ArrayList(statements); statements.clear(); } for (int i = 0; i < tmpList.size(); i++) { WeakReference wr = (WeakReference)tmpList.get(i); if (wr != null) { Statement stmt = (Statement) wr.get(); if (stmt != null) { try { stmt.close(); } catch (SQLException ex) { // Ignore } } } } try { // Tell the server the session is ending, close network connection if (baseTds != null) { baseTds.closeConnection(); baseTds.close(); } // Close cached TdsCore if (cachedTds != null) { cachedTds.close(); cachedTds = null; } } catch (SQLException ex) { // Ignore } if (socket != null) { socket.close(); } } catch (IOException e) { // Ignore } finally { closed = true; synchronized( connections ) { if (--connections[0] == 0) { TimerThread.stopTimer(); } } } } } synchronized public void commit() throws SQLException { checkOpen(); checkLocal("commit"); if (getAutoCommit()) { throw new SQLException( Messages.get("error.connection.autocommit", "commit"), "25000"); } baseTds.submitSQL("IF @@TRANCOUNT > 0 COMMIT TRAN"); procInTran.clear(); clearSavepoints(); } synchronized public void rollback() throws SQLException { checkOpen(); checkLocal("rollback"); if (getAutoCommit()) { throw new SQLException( Messages.get("error.connection.autocommit", "rollback"), "25000"); } baseTds.submitSQL("IF @@TRANCOUNT > 0 ROLLBACK TRAN"); for (int i = 0; i < procInTran.size(); i++) { String key = (String) procInTran.get(i); if (key != null) { statementCache.remove(key); } } procInTran.clear(); clearSavepoints(); } public boolean getAutoCommit() throws SQLException { checkOpen(); return autoCommit; } public boolean isClosed() throws SQLException { return closed; } public boolean isReadOnly() throws SQLException { checkOpen(); return readOnly; } public void setHoldability(int holdability) throws SQLException { checkOpen(); switch (holdability) { case JtdsResultSet.HOLD_CURSORS_OVER_COMMIT: break; case JtdsResultSet.CLOSE_CURSORS_AT_COMMIT: throw new SQLException( Messages.get("error.generic.optvalue", "CLOSE_CURSORS_AT_COMMIT", "setHoldability"), "HY092"); default: throw new SQLException( Messages.get("error.generic.badoption", Integer.toString(holdability), "holdability"), "HY092"); } } synchronized public void setTransactionIsolation(int level) throws SQLException { checkOpen(); if (transactionIsolation == level) { // No need to submit a request return; } String sql = "SET TRANSACTION ISOLATION LEVEL "; boolean sybase = serverType == Driver.SYBASE; switch (level) { case java.sql.Connection.TRANSACTION_READ_UNCOMMITTED: sql += (sybase) ? "0" : "READ UNCOMMITTED"; break; case java.sql.Connection.TRANSACTION_READ_COMMITTED: sql += (sybase) ? "1" : "READ COMMITTED"; break; case java.sql.Connection.TRANSACTION_REPEATABLE_READ: sql += (sybase) ? "2" : "REPEATABLE READ"; break; case java.sql.Connection.TRANSACTION_SERIALIZABLE: sql += (sybase) ? "3" : "SERIALIZABLE"; break; case TRANSACTION_SNAPSHOT: if (sybase) { throw new SQLException( Messages.get("error.generic.optvalue", "TRANSACTION_SNAPSHOT", "setTransactionIsolation"), "HY024"); } else { sql += "SNAPSHOT"; } break; case java.sql.Connection.TRANSACTION_NONE: throw new SQLException( Messages.get("error.generic.optvalue", "TRANSACTION_NONE", "setTransactionIsolation"), "HY024"); default: throw new SQLException( Messages.get("error.generic.badoption", Integer.toString(level), "level"), "HY092"); } transactionIsolation = level; baseTds.submitSQL(sql); } synchronized public void setAutoCommit(boolean autoCommit) throws SQLException { checkOpen(); checkLocal("setAutoCommit"); if (this.autoCommit == autoCommit) { // If we don't need to change the current auto commit mode, don't // submit a request and don't commit either. Section 10.1.1 of the // JDBC 3.0 spec states that the transaction should be committed // only "if the value of auto-commit is _changed_ in the middle of // a transaction". This takes precedence over the API docs, which // states that "if this method is called during a transaction, the // transaction is committed". return; } StringBuilder sql = new StringBuilder(70); // if (!this.autoCommit) { // If we're in manual commit mode the spec requires that we commit // the transaction when setAutoCommit() is called sql.append("IF @@TRANCOUNT > 0 COMMIT TRAN\r\n"); } if (serverType == Driver.SYBASE) { if (autoCommit) { sql.append("SET CHAINED OFF"); } else { sql.append("SET CHAINED ON"); } } else { if (autoCommit) { sql.append("SET IMPLICIT_TRANSACTIONS OFF"); } else { sql.append("SET IMPLICIT_TRANSACTIONS ON"); } } baseTds.submitSQL(sql.toString()); this.autoCommit = autoCommit; } public void setReadOnly(boolean readOnly) throws SQLException { checkOpen(); this.readOnly = readOnly; } synchronized public String getCatalog() throws SQLException { checkOpen(); return currentDatabase; } synchronized public void setCatalog(String catalog) throws SQLException { checkOpen(); if (currentDatabase != null && currentDatabase.equals(catalog)) { return; } int maxlength = tdsVersion >= Driver.TDS70 ? 128 : 30; if (catalog.length() > maxlength || catalog.length() < 1) { throw new SQLException( Messages.get("error.generic.badparam", catalog, "catalog"), "3D000"); } String sql = tdsVersion >= Driver.TDS70 ? ("use [" + catalog + ']') : "use " + catalog; baseTds.submitSQL(sql); } public DatabaseMetaData getMetaData() throws SQLException { checkOpen(); return new JtdsDatabaseMetaData(this); } public SQLWarning getWarnings() throws SQLException { checkOpen(); return messages.getWarnings(); } public Statement createStatement() throws SQLException { checkOpen(); return createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY); } synchronized public Statement createStatement(int type, int concurrency) throws SQLException { checkOpen(); JtdsStatement stmt = new JtdsStatement(this, type, concurrency); addStatement(stmt); return stmt; } public Statement createStatement(int type, int concurrency, int holdability) throws SQLException { checkOpen(); setHoldability(holdability); return createStatement(type, concurrency); } public Map getTypeMap() throws SQLException { checkOpen(); return new HashMap(); } public void setTypeMap(Map map) throws SQLException { checkOpen(); notImplemented("Connection.setTypeMap(Map)"); } public String nativeSQL(String sql) throws SQLException { checkOpen(); if (sql == null || sql.length() == 0) { throw new SQLException(Messages.get("error.generic.nosql"), "HY000"); } String[] result = SQLParser.parse(sql, new ArrayList(), this, false); return result[0]; } public CallableStatement prepareCall(String sql) throws SQLException { checkOpen(); return prepareCall(sql, java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY); } synchronized public CallableStatement prepareCall(String sql, int type, int concurrency) throws SQLException { checkOpen(); if (sql == null || sql.length() == 0) { throw new SQLException(Messages.get("error.generic.nosql"), "HY000"); } JtdsCallableStatement stmt = new JtdsCallableStatement(this, sql, type, concurrency); addStatement(stmt); return stmt; } public CallableStatement prepareCall( String sql, int type, int concurrency, int holdability) throws SQLException { checkOpen(); setHoldability(holdability); return prepareCall(sql, type, concurrency); } public PreparedStatement prepareStatement(String sql) throws SQLException { checkOpen(); return prepareStatement(sql, java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY); } public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { checkOpen(); if (sql == null || sql.length() == 0) { throw new SQLException(Messages.get("error.generic.nosql"), "HY000"); } if (autoGeneratedKeys != JtdsStatement.RETURN_GENERATED_KEYS && autoGeneratedKeys != JtdsStatement.NO_GENERATED_KEYS) { throw new SQLException( Messages.get("error.generic.badoption", Integer.toString(autoGeneratedKeys), "autoGeneratedKeys"), "HY092"); } JtdsPreparedStatement stmt = new JtdsPreparedStatement(this, sql, java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY, autoGeneratedKeys == JtdsStatement.RETURN_GENERATED_KEYS); addStatement(stmt); return stmt; } synchronized public PreparedStatement prepareStatement(String sql, int type, int concurrency) throws SQLException { checkOpen(); if (sql == null || sql.length() == 0) { throw new SQLException(Messages.get("error.generic.nosql"), "HY000"); } JtdsPreparedStatement stmt = new JtdsPreparedStatement(this, sql, type, concurrency, false); addStatement(stmt); return stmt; } public PreparedStatement prepareStatement( String sql, int type, int concurrency, int holdability) throws SQLException { checkOpen(); setHoldability(holdability); return prepareStatement(sql, type, concurrency); } public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { if (columnIndexes == null) { throw new SQLException( Messages.get("error.generic.nullparam", "prepareStatement"),"HY092"); } else if (columnIndexes.length != 1) { throw new SQLException( Messages.get("error.generic.needcolindex", "prepareStatement"),"HY092"); } return prepareStatement(sql, JtdsStatement.RETURN_GENERATED_KEYS); } public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { if (columnNames == null) { throw new SQLException( Messages.get("error.generic.nullparam", "prepareStatement"),"HY092"); } else if (columnNames.length != 1) { throw new SQLException( Messages.get("error.generic.needcolname", "prepareStatement"),"HY092"); } return prepareStatement(sql, JtdsStatement.RETURN_GENERATED_KEYS); } /** * Add a savepoint to the list maintained by this connection. * * @param savepoint * The savepoint object to add. * * @throws SQLException */ private void setSavepoint( SavepointImpl savepoint ) throws SQLException { Statement statement = null; try { statement = createStatement(); if( serverType == Driver.SYBASE ) { statement.execute( "IF @@TRANCOUNT=0 BEGIN TRAN " + "SAVE TRAN jtds" + savepoint.getId() ); } else { statement.execute( "IF @@TRANCOUNT=0 BEGIN " + "SET IMPLICIT_TRANSACTIONS OFF; BEGIN TRAN; " // Fix for bug #569: in "SET IMPLICIT_TRANSACTIONS ON" mode + "SET IMPLICIT_TRANSACTIONS ON; END " // (auto-commit), "BEGIN TRAN" actually starts two transactions! + "SAVE TRAN jtds" + savepoint.getId() ); } } finally { if( statement != null ) { statement.close(); } } synchronized( this ) { if( savepoints == null ) { savepoints = new ArrayList(); } savepoints.add(savepoint); } } /** * Releases all savepoints. Used internally when committing or rolling back * a transaction. */ private synchronized void clearSavepoints() { if (savepoints != null) { savepoints.clear(); } if (savepointProcInTran != null) { savepointProcInTran.clear(); } savepointId = 0; } // JDBC 3 public synchronized void releaseSavepoint(Savepoint savepoint) throws SQLException { checkOpen(); if (savepoints == null) { throw new SQLException( Messages.get("error.connection.badsavep"), "25000"); } int index = savepoints.indexOf(savepoint); if (index == -1) { throw new SQLException( Messages.get("error.connection.badsavep"), "25000"); } Object tmpSavepoint = savepoints.remove(index); if (savepointProcInTran != null) { if (index != 0) { // If this wasn't the outermost savepoint, move all procedures // to the "wrapping" savepoint's list; when and if that // savepoint will be rolled back it will clear these procedures // too List keys = (List) savepointProcInTran.get(savepoint); if (keys != null) { Savepoint wrapping = (Savepoint) savepoints.get(index - 1); List wrappingKeys = (List) savepointProcInTran.get(wrapping); if (wrappingKeys == null) { wrappingKeys = new ArrayList(); } wrappingKeys.addAll(keys); savepointProcInTran.put(wrapping, wrappingKeys); } } // If this was the outermost savepoint, just drop references to // all procedures; they will be managed by the connection savepointProcInTran.remove(tmpSavepoint); } } public synchronized void rollback(Savepoint savepoint) throws SQLException { checkOpen(); checkLocal("rollback"); if (savepoints == null) { throw new SQLException( Messages.get("error.connection.badsavep"), "25000"); } int index = savepoints.indexOf(savepoint); if (index == -1) { throw new SQLException( Messages.get("error.connection.badsavep"), "25000"); } else if (getAutoCommit()) { throw new SQLException( Messages.get("error.connection.savenorollback"), "25000"); } Statement statement = null; try { statement = createStatement(); statement.execute("ROLLBACK TRAN jtds" + ((SavepointImpl) savepoint).getId()); } finally { if (statement != null) { statement.close(); } } int size = savepoints.size(); for (int i = size - 1; i >= index; i--) { Object tmpSavepoint = savepoints.remove(i); if (savepointProcInTran == null) { continue; } List keys = (List) savepointProcInTran.get(tmpSavepoint); if (keys == null) { continue; } for (Iterator iterator = keys.iterator(); iterator.hasNext();) { String key = (String) iterator.next(); removeCachedProcedure(key); } } // recreate savepoint setSavepoint((SavepointImpl) savepoint); } synchronized public Savepoint setSavepoint() throws SQLException { checkOpen(); checkLocal("setSavepoint"); if (getAutoCommit()) { throw new SQLException( Messages.get("error.connection.savenoset"), "25000"); } SavepointImpl savepoint = new SavepointImpl(getNextSavepointId()); setSavepoint(savepoint); return savepoint; } synchronized public Savepoint setSavepoint(String name) throws SQLException { checkOpen(); checkLocal("setSavepoint"); if (getAutoCommit()) { throw new SQLException( Messages.get("error.connection.savenoset"), "25000"); } else if (name == null) { throw new SQLException( Messages.get("error.connection.savenullname", "savepoint"), "25000"); } SavepointImpl savepoint = new SavepointImpl(getNextSavepointId(), name); setSavepoint(savepoint); return savepoint; } /** * Returns the next savepoint identifier. * * @return the next savepoint identifier */ private int getNextSavepointId() { return ++savepointId; } /** * Add a stored procedure to the savepoint cache. * * @param key The signature of the procedure to cache. */ synchronized void addCachedProcedure(String key) { if (savepoints == null || savepoints.size() == 0) { return; } if (savepointProcInTran == null) { savepointProcInTran = new HashMap(); } // Retrieve the current savepoint Object savepoint = savepoints.get(savepoints.size() - 1); List keys = (List) savepointProcInTran.get(savepoint); if (keys == null) { keys = new ArrayList(); } keys.add(key); savepointProcInTran.put(savepoint, keys); } /////// JDBC4 demarcation, do NOT put any JDBC3 code below this line /////// /* (non-Javadoc) * @see java.sql.Connection#createArrayOf(java.lang.String, java.lang.Object[]) */ public Array createArrayOf(String typeName, Object[] elements) throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Connection#createBlob() */ public Blob createBlob() throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Connection#createClob() */ public Clob createClob() throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Connection#createNClob() */ public NClob createNClob() throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Connection#createSQLXML() */ public SQLXML createSQLXML() throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Connection#createStruct(java.lang.String, java.lang.Object[]) */ public Struct createStruct(String typeName, Object[] attributes) throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Connection#getClientInfo() */ public Properties getClientInfo() throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Connection#getClientInfo(java.lang.String) */ public String getClientInfo(String name) throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Connection#isValid(int) */ public boolean isValid(int timeout) throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Connection#setClientInfo(java.util.Properties) */ public void setClientInfo(Properties properties) throws SQLClientInfoException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Connection#setClientInfo(java.lang.String, java.lang.String) */ public void setClientInfo(String name, String value) throws SQLClientInfoException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Wrapper#isWrapperFor(java.lang.Class) */ public boolean isWrapperFor(Class arg0) throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Wrapper#unwrap(java.lang.Class) */ public Object unwrap(Class arg0) throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } //// JDBC4.1 demarcation, do NOT put any JDBC3/4.0 code below this line //// @Override public void setSchema(String schema) throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } @Override public String getSchema() throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } @Override public void abort(java.util.concurrent.Executor executor) throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } @Override public void setNetworkTimeout(java.util.concurrent.Executor executor, int milliseconds) throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } @Override public int getNetworkTimeout() throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } }