// 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.io.File;
import java.io.FileInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.*;
import java.util.Properties;
import java.util.Enumeration;
import junit.framework.Test;
import junit.framework.TestSuite;
import net.sourceforge.jtds.jdbc.JtdsConnection;
import net.sourceforge.jtds.jdbc.DefaultProperties;
import net.sourceforge.jtds.jdbc.Messages;
/**
* Unit test for the {@link JtdsConnection} class.
*
* @author
* David Kilzer, Alin Sinpalean, Holger Rehn
*/
public class ConnectionTest extends UnitTestBase
{
/**
* <p> Construct a test suite for this class. </p>
*
* The test suite includes the tests in this class, and adds tests from
* {@link DefaultPropertiesTestLibrary} after creating an anonymous
* {@link DefaultPropertiesTester} object.
*
* @return
* The test suite to run.
*/
public static Test suite()
{
final TestSuite testSuite = new TestSuite( ConnectionTest.class );
testSuite.addTest( ConnectionTest.TestUnpackProperties.suite( "testUnpackProperties" ) );
return testSuite;
}
/**
* <p> Constructor. </p>
*
* @param name
* name of the test
*/
public ConnectionTest( String name )
{
super( name );
}
/**
* Test that an {@link java.sql.SQLException} is thrown when parsing invalid
* integer (and long) properties.
*/
public void testInvalidIntegerProperty()
{
assertSQLExceptionForBadWholeNumberProperty( Driver.PORTNUMBER );
assertSQLExceptionForBadWholeNumberProperty( Driver.SERVERTYPE );
assertSQLExceptionForBadWholeNumberProperty( Driver.PREPARESQL );
assertSQLExceptionForBadWholeNumberProperty( Driver.PACKETSIZE );
assertSQLExceptionForBadWholeNumberProperty( Driver.LOGINTIMEOUT );
assertSQLExceptionForBadWholeNumberProperty( Driver.LOBBUFFER );
}
/**
* Class used to test
* <code>net.sourceforge.jtds.jdbc.JtdsConnection.unpackProperties(Properties)</code>
* .
*/
public static class TestUnpackProperties extends DefaultPropertiesTestLibrary
{
/**
* Construct a test suite for this library.
*
* @param name
* The name of the tests.
* @return The test suite.
*/
public static Test suite( String name )
{
return new TestSuite( ConnectionTest.TestUnpackProperties.class, name );
}
/**
* Default constructor.
*/
public TestUnpackProperties()
{
setTester( new DefaultPropertiesTester()
{
@Override
public void assertDefaultProperty( String message, String url, Properties properties, String fieldName, String key, String expected )
{
// FIXME: Hack for JtdsConnection
{
if( "sendStringParametersAsUnicode".equals( fieldName ) )
{
fieldName = "useUnicode";
}
else if( "cacheMetaData".equals( fieldName ) )
{
fieldName = "useMetadataCache";
}
}
Properties parsedProperties = (Properties) invokeStaticMethod( Driver.class, "parseURL", new Class[] { String.class, Properties.class }, new Object[] { url, properties } );
parsedProperties = (Properties) invokeStaticMethod( DefaultProperties.class, "addDefaultProperties", new Class[] { Properties.class }, new Object[] { parsedProperties } );
JtdsConnection instance = (JtdsConnection) invokeConstructor( JtdsConnection.class, new Class[] {}, new Object[] {} );
invokeInstanceMethod( instance, "unpackProperties", new Class[] { Properties.class }, new Object[] { parsedProperties } );
String actual = String.valueOf( invokeInstanceMethod( instance, "get" + ucFirst( fieldName ), new Class[] {}, new Object[] {} ) );
// FIXME: Another hack for JtdsConnection
{
if( "tdsVersion".equals( fieldName ) )
{
expected = String.valueOf( DefaultProperties.getTdsVersion( expected ) );
}
}
assertEquals( message, expected, actual );
}
} );
}
}
/**
* Test correct behavior of the <code>charset</code> property. Values should
* be stored and retrieved using the requested charset rather than the
* server's as long as Unicode is not used.
*/
public void testForceCharset1() throws Exception
{
// Set charset to Cp1251 and Unicode parameters to false
Properties props = new Properties();
props.setProperty( Messages.get( Driver.CHARSET ), "Cp1251" );
props.setProperty( Messages.get( Driver.SENDSTRINGPARAMETERSASUNICODE ), "false" );
// Obtain connection
Connection con = getConnectionOverrideProperties( props );
try
{
// Test both sending and retrieving of values
String value = "\u0410\u0411\u0412";
PreparedStatement pstmt = con.prepareStatement( "select ?" );
pstmt.setString( 1, value );
ResultSet rs = pstmt.executeQuery();
assertTrue( rs.next() );
assertEquals( value, rs.getString( 1 ) );
assertFalse( rs.next() );
rs.close();
pstmt.close();
}
finally
{
con.close();
}
}
/**
* Test correct behavior of the <code>charset</code> property. Stored
* procedure output parameters should be decoded using the specified charset
* rather than the server's as long as they are non-Unicode.
*/
public void testForceCharset2()
throws Exception
{
dropProcedure( "testForceCharset2" );
// Set charset to Cp1251 and Unicode parameters to false
Properties props = new Properties();
props.setProperty( Messages.get( Driver.CHARSET ), "Cp1251" );
props.setProperty( Messages.get( Driver.SENDSTRINGPARAMETERSASUNICODE ), "false" );
// Obtain connection
Connection con = getConnectionOverrideProperties( props );
try
{
Statement stmt = con.createStatement();
assertEquals( 0, stmt.executeUpdate( "create procedure testForceCharset2 " + "@inParam varchar(10), @outParam varchar(10) output as " + "set @outParam = @inParam" ) );
stmt.close();
// Test both sending and retrieving of parameters
String value = "\u0410\u0411\u0412";
CallableStatement cstmt = con.prepareCall( "{call testForceCharset2(?, ?)}" );
cstmt.setString( 1, value );
cstmt.registerOutParameter( 2, Types.VARCHAR );
assertEquals( 0, cstmt.executeUpdate() );
assertEquals( value, cstmt.getString( 2 ) );
cstmt.close();
}
finally
{
con.close();
}
}
/**
* Test for bug [1296482] setAutoCommit() behaviour.
* <p/>
* The behaviour of setAutoCommit() on JtdsConnection is inconsistent with
* the Sun JDBC 3.0 Specification. JDBC 3.0 Specification, section 10.1.1:
* <blockquote>"If the value of auto-commit is changed in the middle of a
* transaction, the current transaction is committed."</blockquote>
*/
public void testAutoCommit() throws Exception
{
Connection con = getConnectionOverrideProperties( new Properties() );
try
{
Statement stmt = con.createStatement();
// Create temp table
assertEquals( 0, stmt.executeUpdate( "create table #testAutoCommit (i int)" ) );
// Manual commit mode
con.setAutoCommit( false );
// Insert one row
assertEquals( 1, stmt.executeUpdate( "insert into #testAutoCommit (i) values (0)" ) );
// Set commit mode to manual again; should have no effect
con.setAutoCommit( false );
// Rollback the transaction; should roll back the insert
con.rollback();
// Insert one more row
assertEquals( 1, stmt.executeUpdate( "insert into #testAutoCommit (i) values (1)" ) );
// Set commit mode to automatic; should commit everything
con.setAutoCommit( true );
// Go back to manual commit mode
con.setAutoCommit( false );
// Rollback transaction; should do nothing
con.rollback();
// And back to auto commit mode again
con.setAutoCommit( true );
// Now see if the second row is there
ResultSet rs = stmt.executeQuery( "select i from #testAutoCommit" );
assertTrue( rs.next() );
assertEquals( 1, rs.getInt( 1 ) );
assertFalse( rs.next() );
// We're done, close everything
rs.close();
stmt.close();
}
finally
{
con.close();
}
}
/**
* Regression test for bug #673, function expansion causes buffer overflow.
*/
public void testBug673() throws Exception
{
Connection con = getConnection();
Statement stmt = con.createStatement();
stmt.execute( "SELECT {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}, {fn curdate()}" );
}
/**
* Test that temporary procedures created within transactions with savepoints
* which are released are still kept in the procedure cache.
*
* @test.manual
* when testing, prepareSQL will have to be set to 1 to make sure temp procedures are used
*/
public void testSavepointRelease() throws SQLException
{
// Manual commit mode
con.setAutoCommit( false );
// Create two savepoints
Savepoint sp1 = con.setSavepoint();
Savepoint sp2 = con.setSavepoint();
// Create and execute a prepared statement
PreparedStatement stmt = con.prepareStatement( "SELECT 1" );
assertTrue( stmt.execute() );
// Release the inner savepoint and rollback the outer
con.releaseSavepoint( sp2 );
con.rollback( sp1 );
// Now make sure the temp stored procedure still exists
assertTrue( stmt.execute() );
// Release resources
stmt.close();
con.close();
}
/**
* Test for bug [1755448], login failure leaves unclosed sockets.
*/
public void testUnclosedSocket()
{
final int count = 100;
String url = props.getProperty( "url" ) + ";loginTimeout=600";
Properties p = new Properties( props );
p.put( "PASSWORD", "invalid_password" );
p.put( "loginTimeout", "60" );
for( int i = 0; i < count; i++ )
{
try
{
DriverManager.getConnection( url, p );
assertTrue( false );
}
catch( SQLException e )
{
assertEquals( 18456, e.getErrorCode() );
}
}
}
/**
* Test for bug [2871274], TimerThread prevents classloader from being GCed.
*/
public void testTimerStop() throws Throwable
{
// number of load/unload cycles (use large numbers > 1000 for real stress
// test)
int RELOADS = 10;
// counter for GCed class loaders
final int[] counter = new int[] { 0 };
try
{
// run the test RELOADS times to ensure everything is GCed correctly
for( int i = 0; i < RELOADS; i++ )
{
// create new classloader for loading the actual test
ClassLoader cloader = new URLClassLoader( new URL[] { new File( "bin" ).toURI().toURL() }, null )
{
@Override
protected void finalize() throws Throwable
{
counter[0]++;
super.finalize();
}
};
// load the actual test class
Class clazz = cloader.loadClass( testTimerStopHelper.class.getName() );
Constructor constructor = clazz.getDeclaredConstructor( (Class[]) null );
// start the test by
try
{
constructor.newInstance( (Object[]) null );
}
catch( InvocationTargetException e )
{
// extract target exception
throw e.getTargetException();
}
}
// squeeze out any remaining class loaders
for( int i = 0; i < 10; i++ )
{
System.gc();
System.runFinalization();
}
// ensure some of the created classloaders have been GCed at all
assertTrue( "jTDS prevented its classloader from being GCed", counter[0] > 0 );
// ensure that any of the created classloaders has been GCed
assertEquals( "not all of jTDS' classloaders have been GCed", RELOADS, counter[0] );
}
catch( OutOfMemoryError oome )
{
fail( "jTDS leaked memory, maybe its classloaders could not be GCed" );
}
}
/**
* Helper class for test for bug [2871274].
*/
public static class testTimerStopHelper
{
/**
* Constructor for helper class, simply starts method {@link #test()}.
*/
public testTimerStopHelper() throws Throwable
{
test();
}
/**
* The actual test, creates and closes a number of connections.
*/
public void test() throws Exception
{
// load driver
Class.forName( "net.sourceforge.jtds.jdbc.Driver" );
// load connection properties
Properties p = loadProperties();
Connection[] conns = new Connection[5];
// create a number of connections
for( int c = 0; c < conns.length; c++ )
{
conns[c] = DriverManager.getConnection( p.getProperty( "url" ), p );
}
// close the previously created connections
for( int c = 0; c < conns.length; c++ )
{
conns[c].close();
}
// remove driver from DriverManager
Enumeration e = DriverManager.getDrivers();
while( e.hasMoreElements() )
{
java.sql.Driver d = (java.sql.Driver) e.nextElement();
if( d.getClass().getName().equals( "net.sourceforge.jtds.jdbc.Driver" ) )
{
DriverManager.deregisterDriver( d );
break;
}
}
// the class loader should be ready for GC now
}
/**
* Loads the connection properties from config file.
*/
private static Properties loadProperties() throws Exception
{
File propFile = new File( "conf/connection.properties" );
if( !propFile.exists() ) fail( "Connection properties not found (" + propFile + ")." );
Properties props = new Properties();
props.load( new FileInputStream( propFile ) );
props.put( "loginTimeout", "60" );
return props;
}
}
/**
* Assert that an SQLException is thrown when
* {@link JtdsConnection#unpackProperties(Properties)} is called with an
* invalid integer (or long) string set on a property.
* <p/>
* Note that because Java 1.3 is still supported, the
* {@link RuntimeException} that is caught may not contain the original
* {@link Throwable} cause, only the original message.
*
* @param key
* The message key used to retrieve the property name.
*/
private void assertSQLExceptionForBadWholeNumberProperty( final String key )
{
final JtdsConnection instance = (JtdsConnection) invokeConstructor( JtdsConnection.class, new Class[] {}, new Object[] {} );
Properties properties = (Properties) invokeStaticMethod( Driver.class, "parseURL", new Class[] { String.class, Properties.class }, new Object[] { "jdbc:jtds:sqlserver://servername", new Properties() } );
properties = (Properties) invokeStaticMethod( DefaultProperties.class, "addDefaultProperties", new Class[] { Properties.class }, new Object[] { properties } );
properties.setProperty( Messages.get( key ), "1.21 Gigawatts" );
try
{
invokeInstanceMethod( instance, "unpackProperties", new Class[] { Properties.class }, new Object[] { properties } );
fail( "RuntimeException expected" );
}
catch( RuntimeException e )
{
assertEquals( "Unexpected exception message", Messages.get( "error.connection.badprop", Messages.get( key ) ), e.getMessage() );
}
}
/**
* Creates a <code>Connection</code>, overriding the default properties with
* the ones provided.
*
* @param override
* the overriding properties
*
* @return
* a <code>Connection</code> object
*/
private Connection getConnectionOverrideProperties( Properties override ) throws Exception
{
// Get properties, override with provided values
Properties props = (Properties) TestBase.props.clone();
for( Enumeration e = override.keys(); e.hasMoreElements(); )
{
String key = (String) e.nextElement();
props.setProperty( key, override.getProperty( key ) );
}
// Obtain connection
Class.forName( props.getProperty( "driver" ) );
String url = props.getProperty( "url" );
return DriverManager.getConnection( url, props );
}
}