1   /*
2    * Copyright (c) 1998, 2005 Gargoyle Software Inc. All rights reserved.
3    *
4    * Redistribution and use in source and binary forms, with or without
5    * modification, are permitted provided that the following conditions are met:
6    *
7    * 1. Redistributions of source code must retain the above copyright notice,
8    *    this list of conditions and the following disclaimer.
9    * 2. Redistributions in binary form must reproduce the above copyright notice,
10   *    this list of conditions and the following disclaimer in the documentation
11   *    and/or other materials provided with the distribution.
12   * 3. The end-user documentation included with the redistribution, if any, must
13   *    include the following acknowledgment:
14   *
15   *       "This product includes software developed by Gargoyle Software Inc.
16   *        (http://www.GargoyleSoftware.com/)."
17   *
18   *    Alternately, this acknowledgment may appear in the software itself, if
19   *    and wherever such third-party acknowledgments normally appear.
20   * 4. The name "Gargoyle Software" must not be used to endorse or promote
21   *    products derived from this software without prior written permission.
22   *    For written permission, please contact info@GargoyleSoftware.com.
23   * 5. Products derived from this software may not be called "GSBase", nor may
24   *    "GSBase" appear in their name, without prior written permission of
25   *    Gargoyle Software Inc.
26   *
27   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
28   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
29   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GARGOYLE
30   * SOFTWARE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
31   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
32   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
33   * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
34   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
35   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
36   * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37   */
38  package com.gargoylesoftware.base.resource.jdbc;
39  
40  import com.gargoylesoftware.base.util.DetailedIllegalArgumentException;
41  import java.lang.reflect.Constructor;
42  import java.lang.reflect.InvocationHandler;
43  import java.lang.reflect.InvocationTargetException;
44  import java.lang.reflect.Method;
45  import java.lang.reflect.Proxy;
46  import java.net.MalformedURLException;
47  import java.net.URL;
48  import java.sql.CallableStatement;
49  import java.sql.Connection;
50  import java.sql.DatabaseMetaData;
51  import java.sql.ParameterMetaData;
52  import java.sql.PreparedStatement;
53  import java.sql.ResultSet;
54  import java.sql.SQLException;
55  import java.sql.SQLWarning;
56  import java.sql.Savepoint;
57  import java.sql.Statement;
58  import java.util.ArrayList;
59  import java.util.HashMap;
60  import java.util.List;
61  import java.util.Map;
62  import junit.framework.TestCase;
63  import junit.framework.TestSuite;
64  
65  /***
66   *  A TestCase that dynamically generates a seperate test for each method in a
67   *  specified interface. This is used to test the various jdbc wrappers.
68   *
69   * @version  $Revision: 1.5 $
70   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
71   */
72  public class WrapperTestCase extends TestCase {
73  
74      private final Method method_;
75      private final Class interfaceToTest_;
76      private final Constructor wrapperConstructor_;
77      private final boolean isResultWrapped_;
78  
79  
80      /***
81       *  Create an instance
82       *
83       * @param  testName
84       * @param  interfaceToTest
85       * @param  method
86       * @param  wrapperConstructor
87       * @param  isResultWrapped
88       */
89      private WrapperTestCase(
90              final String testName,
91              final Class interfaceToTest,
92              final Method method,
93              final Constructor wrapperConstructor,
94              final boolean isResultWrapped ) {
95  
96          super( testName );
97          method_ = method;
98          interfaceToTest_ = interfaceToTest;
99          wrapperConstructor_ = wrapperConstructor;
100         isResultWrapped_ = isResultWrapped;
101     }
102 
103 
104     /***
105      *  Create a test suite containing a seperate test for each method in
106      *  'interfaceToTest'
107      *
108      * @param  interfaceToTest The interface being tested
109      * @param  wrapperClass The concrete class that we are testing. This
110      *      implements the interface 'interfaceToTest'
111      * @param  excludedMethods An array of method names. These methods will not
112      *      be tested.
113      * @param  wrappedMethods An array of method names. These methods are
114      *      expected to return wrapped objects which contain the original result
115      * @return  A test suite
116      */
117     public static TestSuite getWrapperTestSuite(
118             final Class interfaceToTest,
119             final Class wrapperClass,
120             final String excludedMethods[],
121             final String wrappedMethods[] ) {
122 
123         if( interfaceToTest == null ) {
124             throw new NullPointerException( "interfaceToTest" );
125         }
126         if( excludedMethods == null ) {
127             throw new NullPointerException( "excludedMethods" );
128         }
129         if( wrappedMethods == null ) {// Implement support for this
130             throw new NullPointerException( "wrappedMethods" );
131         }
132         if( wrapperClass == null ) {
133             throw new NullPointerException( "wrapperClass" );
134         }
135 
136         final Constructor wrapperConstructor;
137         try {
138             wrapperConstructor = wrapperClass.getConstructor( new Class[]{interfaceToTest} );
139         }
140         catch( final NoSuchMethodException e ) {
141             throw new DetailedIllegalArgumentException(
142                 "wrapperClass", wrapperClass,
143                 "Does not have a constructor that takes an instance of "+interfaceToTest.getName() );
144         }
145 
146         String testName;
147 
148         final TestSuite suite = new TestSuite();
149         final Method methods[] = interfaceToTest.getMethods();
150         int i;
151         for( i = 0; i < methods.length; i++ ) {
152             if( contains( methods[i], excludedMethods ) == false ) {
153                 testName = createTestName( wrapperClass, methods[i] );
154                 suite.addTest( new WrapperTestCase( testName,
155                         interfaceToTest,
156                         methods[i],
157                         wrapperConstructor,
158                         contains( methods[i], wrappedMethods ) ) );
159             }
160         }
161 
162         return suite;
163     }
164 
165 
166     private static String createTestName( final Class wrapperClass, final Method method ) {
167         final StringBuffer buffer = new StringBuffer( 100 );
168         buffer.append( "Method: " );
169         buffer.append( wrapperClass.getName() );
170         buffer.append( '.' );
171         buffer.append( method.getName() );
172         buffer.append( '(' );
173 
174         final Class parameterClasses[] = method.getParameterTypes();
175 
176         int i;
177         for( i = 0; i < parameterClasses.length; i++ ) {
178             if( i != 0 ) {
179                 buffer.append( "," );
180             }
181 
182             buffer.append( parameterClasses[i].getName() );
183         }
184         buffer.append( ")" );
185         return buffer.toString();
186     }
187 
188 
189     private static boolean contains( final Method method, final String array[] ) {
190         final String methodName = method.getName();
191 
192         int i;
193         for( i = 0; i < array.length; i++ ) {
194             if( methodName.equals( array[i] ) ) {
195                 return true;
196             }
197         }
198 
199         return false;
200     }
201 
202 
203     /***
204      *  During normal operation, the wrapper should allow all method calls to
205      *  pass through to the wrapped object. Verify that this is working
206      *  correctly.
207      *
208      * @exception  Exception If an error occurs
209      */
210     protected void runTest()
211         throws Exception {
212 
213         final Method methods[] = interfaceToTest_.getMethods();
214         assertTrue( "the interface has methods", methods.length != 0 );
215 
216         final List list = new ArrayList();
217         final InvocationHandler handler =
218             new InvocationHandler() {
219                 public Object invoke( Object proxy, Method method, Object[] args ) {
220                     if( method.getName().equals( "toString" ) ) {
221                         return "Proxy(" + interfaceToTest_.getClass().getName() + ")";
222                     }
223 
224                     list.add( method );
225                     if( args != null ) {
226                         int i;
227                         for( i = 0; i < args.length; i++ ) {
228                             list.add( args[i] );
229                         }
230                     }
231 
232                     if( method.getReturnType().equals( void.class ) ) {
233                         return null;
234                     }
235 
236                     final Object rc = makeObject( method.getReturnType() );
237                     list.add( rc );
238                     return rc;
239                 }
240             };
241 
242         final Object proxy = Proxy.newProxyInstance(
243                 getClass().getClassLoader(),
244                 new Class[]{interfaceToTest_},
245                 handler );
246         final Object testObject = wrapperConstructor_.newInstance( new Object[]{proxy} );
247 
248         testPassthrough( testObject, method_, list );
249 
250         closeObject( testObject );
251 
252         try {
253             testPassthrough( testObject, method_, list );
254             fail( "Expected exception when invoking method on a closed object" );
255         }
256         catch( final InvocationTargetException e ) {
257             assertTrue( e.getTargetException() instanceof SQLException );
258         }
259     }
260 
261 
262     private void testPassthrough(
263             final Object testObject,
264             final Method method,
265             final List list )
266         throws
267             Exception {
268 
269         list.clear();
270 
271         if( false ) {
272             System.out.println( "ConnectionWrapperTest.testPassthrough() method=" + method );
273         }
274 
275         final Class parameterTypes[] = method.getParameterTypes();
276         final Object parameters[] = new Object[parameterTypes.length];
277 
278         int i;
279         for( i = 0; i < parameterTypes.length; i++ ) {
280             parameters[i] = makeObject( parameterTypes[i] );
281         }
282 
283         final Object rc = method.invoke( testObject, parameters );
284 
285         final boolean methodHasReturn = ( method.getReturnType().equals( void.class ) == false );
286         int expectedListSize = parameters.length + 1;
287         if( methodHasReturn ) {
288             expectedListSize++;
289         }
290         assertEquals( "list.size", expectedListSize, list.size() );
291 
292         assertEquals( "method", method, list.get( 0 ) );
293         for( i = 0; i < parameters.length; i++ ) {
294             assertEquals( "Parameter[" + i + "]", parameters[i], list.get( i + 1 ) );
295         }
296 
297         if( methodHasReturn ) {
298             final Object expectedResult = list.get( i + 1 );
299             assertNotNull( "expectedResult", expectedResult );
300             if( isResultWrapped_ ) {
301                 Method getDelegateMethod = null;
302                 try {
303                     getDelegateMethod = rc.getClass().getMethod( "getDelegate", new Class[0] );
304                 }
305                 catch( final NoSuchMethodException e ) {
306                     fail( "Returned object doesn't have a getDelegate() method.  Likely it isn't a wrapper." );
307                 }
308                 final Object delegate = getDelegateMethod.invoke( rc, new Object[0] );
309 
310                 assertEquals( "returned delegate", expectedResult, delegate );
311             }
312             else {
313                 assertEquals( "returned object", expectedResult, rc );
314             }
315         }
316 
317     }
318 
319 
320     private Object makeObject( final Class clazz ) {
321         if( clazz == int.class ) {
322             return new Integer( 5 );
323         }
324         if( clazz == double.class ) {
325             return new Double( 5.0 );
326         }
327         if( clazz == float.class ) {
328             return new Float( 5 );
329         }
330         if( clazz == byte.class ) {
331             return new Byte( (byte)5 );
332         }
333         if( clazz == short.class ) {
334             return new Short( (short)5 );
335         }
336         if( clazz == long.class ) {
337             return new Long( 5 );
338         }
339         if( clazz == boolean.class ) {
340             return new Boolean( true );
341         }
342         if( clazz == void.class ) {
343             return null;
344         }
345         if( clazz == String.class ) {
346             return new String( "" );
347         }
348         if( clazz == Map.class ) {
349             return new HashMap( 3 );
350         }
351         if( clazz == SQLWarning.class ) {
352             return new SQLWarning();
353         }
354         if( clazz == int[].class ) {
355             return new int[]{1, 2, 3};
356         }
357         if( clazz == byte[].class ) {
358             return new byte[]{};
359         }
360         if( clazz == String[].class ) {
361             return new String[]{};
362         }
363         if( clazz == URL.class ) {
364             try {
365                 return new URL("http://foo.com");
366             }
367             catch( final MalformedURLException cantHappenButCompilerRequiresUsToCatchSomething ) {
368                 cantHappenButCompilerRequiresUsToCatchSomething.printStackTrace();
369             }
370         }
371 
372         if( clazz == Statement.class ) {
373             return makeEmptyProxy( clazz );
374         }
375         if( clazz == PreparedStatement.class ) {
376             return makeEmptyProxy( clazz );
377         }
378         if( clazz == CallableStatement.class ) {
379             return makeEmptyProxy( clazz );
380         }
381         if( clazz == DatabaseMetaData.class ) {
382             return makeEmptyProxy( clazz );
383         }
384         if( clazz == Connection.class ) {
385             return makeEmptyProxy( clazz );
386         }
387         if( clazz == ResultSet.class ) {
388             return makeEmptyProxy( clazz );
389         }
390         if( clazz == Savepoint.class ) {
391             return makeEmptyProxy( clazz );
392         }
393         if( clazz == ParameterMetaData.class ) {
394             return makeEmptyProxy( clazz );
395         }
396 
397         if( clazz == java.util.Calendar.class ) {
398             return java.util.Calendar.getInstance();
399         }
400         if( clazz == java.math.BigDecimal.class ) {
401             return new java.math.BigDecimal( 1 );
402         }
403         if( clazz == java.sql.Date.class ) {
404             return new java.sql.Date( 1 );
405         }
406         if( clazz == java.sql.Time.class ) {
407             return new java.sql.Time( 1 );
408         }
409         if( clazz == java.sql.Timestamp.class ) {
410             return new java.sql.Timestamp( 1 );
411         }
412         if( clazz == java.sql.Array.class ) {
413             return makeEmptyProxy( clazz );
414         }
415         if( clazz == java.sql.Ref.class ) {
416             return makeEmptyProxy( clazz );
417         }
418         if( clazz == java.sql.Clob.class ) {
419             return makeEmptyProxy( clazz );
420         }
421         if( clazz == java.sql.Blob.class ) {
422             return makeEmptyProxy( clazz );
423         }
424         if( clazz == java.sql.ResultSetMetaData.class ) {
425             return makeEmptyProxy( clazz );
426         }
427         if( clazz == java.io.InputStream.class ) {
428             return new java.io.ByteArrayInputStream( new byte[0] );
429         }
430         if( clazz == java.io.Reader.class ) {
431             return new java.io.StringReader( "foo" );
432         }
433         if( clazz == Object.class ) {
434             return new Object();
435         }
436 
437         System.out.println( "WrapperTestCase.makeObject(" + clazz.getName() + ")" );
438         return null;
439     }
440 
441 
442     private Object makeEmptyProxy( final Class clazz ) {
443         final InvocationHandler handler =
444             new InvocationHandler() {
445                 public Object invoke( Object proxy, Method method, Object[] args ) {
446                     final String methodName = method.getName();
447 
448                     if( methodName.equals( "equals" ) && args.length == 1 ) {
449                         return new Boolean( proxy == args[0] );
450                     }
451                     else if( methodName.equals( "toString" ) ) {
452                         return "Proxy(" + clazz.getName() + ")";
453                     }
454                     return null;
455                 }
456             };
457 
458         return Proxy.newProxyInstance(
459                 getClass().getClassLoader(),
460                 new Class[]{clazz},
461                 handler );
462     }
463 
464 
465     private void closeObject( final Object object )
466         throws Exception {
467         final Method closeMethod = object.getClass().getMethod( "close", new Class[0] );
468         closeMethod.invoke( object, new Object[0] );
469     }
470 }
471