├── .gitignore ├── hacking.md ├── src ├── test │ ├── resources │ │ ├── oracle │ │ │ ├── 01_users.sql │ │ │ └── 02_permissions.sql │ │ ├── mariadb │ │ │ └── 01_permissions.sql │ │ ├── run_postgres.sh │ │ ├── sql │ │ │ ├── db2_procedures.sql │ │ │ ├── mariadb_procedures.sql │ │ │ ├── mysql_procedures.sql │ │ │ ├── mssql_procedures.sql │ │ │ ├── firebird_procedures.sql │ │ │ ├── hsql_procedures.sql │ │ │ ├── derby_procedures.sql │ │ │ ├── h2_procedures.sql │ │ │ ├── oracle_procedures.sql │ │ │ └── postgres_procedures.sql │ │ ├── run_mysql.sh │ │ ├── run_sql_server.sh │ │ ├── log4j2-test.xml │ │ ├── run_db2.sh │ │ ├── run_firebird.sh │ │ ├── run_mariadb.sh │ │ └── run_oracle.sh │ └── java │ │ └── com │ │ └── github │ │ └── marschall │ │ └── storedprocedureproxy │ │ ├── Travis.java │ │ ├── NoResourceTest.java │ │ ├── NoResourceFactoryTest.java │ │ ├── VoidResultExtractorTest.java │ │ ├── JavaVersionSupport.java │ │ ├── ArrayFactoryTest.java │ │ ├── ScalarResultExtractorTest.java │ │ ├── NoInParameterRegistrationTest.java │ │ ├── OracleArrayFactoryTest.java │ │ ├── NoOutParameterRegistrationTest.java │ │ ├── OracleArrayResultExtractorTest.java │ │ ├── procedures │ │ ├── MssqlProcedures.java │ │ ├── Db2Procedures.java │ │ ├── OracleProcedures.java │ │ ├── HsqlProcedures.java │ │ ├── FirebirdProcedures.java │ │ ├── MysqlProcedures.java │ │ ├── MariaDBProcedures.java │ │ ├── SamplePackage.java │ │ ├── DerbyProcedures.java │ │ ├── OraclePackageProcedures.java │ │ ├── H2Procedures.java │ │ └── PostgresProcedures.java │ │ ├── ByIndexInParameterRegistrationTest.java │ │ ├── ByNameInParameterRegistrationTest.java │ │ ├── PrefixByIndexInParameterRegistrationTest.java │ │ ├── SuffixByIndexInParameterRegistrationTest.java │ │ ├── DerbyProcedureSources.java │ │ ├── ByIndexOutParameterRegistrationTest.java │ │ ├── ByNameOutParameterRegistrationTest.java │ │ ├── ArrayResourceTest.java │ │ ├── DisabledOnTravis.java │ │ ├── ByNameAndTypeInParameterRegistrationTest.java │ │ ├── ByIndexAndTypeInParameterRegistrationTest.java │ │ ├── AbstractDataSourceTest.java │ │ ├── AllParametersRegistrationTest.java │ │ ├── IndexedParametersRegistrationTest.java │ │ ├── configuration │ │ ├── TestConfiguration.java │ │ ├── Db2Configuration.java │ │ ├── H2Configuration.java │ │ ├── DerbyConfiguration.java │ │ ├── HsqlConfiguration.java │ │ ├── MariaDBConfiguration.java │ │ ├── OracleConfiguration.java │ │ ├── MssqlConfiguration.java │ │ ├── FirebirdConfiguration.java │ │ ├── PostgresConfiguration.java │ │ └── MysqlConfiguration.java │ │ ├── ListResultExtractorTest.java │ │ ├── ByteUtilsTest.java │ │ ├── ToStringUtilsTest.java │ │ ├── SpringSQLExceptionAdapterTest.java │ │ ├── Usage.java │ │ ├── H2ProcedureSources.java │ │ ├── DisabledOnTravisCondition.java │ │ ├── MethodHandleTest.java │ │ ├── DefaultTypeNameResolverTest.java │ │ ├── MssqlTest.java │ │ ├── Db2Test.java │ │ ├── DerbyTest.java │ │ ├── MysqlTest.java │ │ ├── ObjectMethodsTest.java │ │ ├── MariaDBTest.java │ │ ├── FirebirdTest.java │ │ ├── H2IT.java │ │ ├── DefaultMethodTest.java │ │ ├── spi │ │ └── NamingStrategyTest.java │ │ ├── HsqlTest.java │ │ ├── AnnotationBasedTypeNameResolverTest.java │ │ ├── ValueExtractorResultExtractorTest.java │ │ ├── ByNameAndTypeNameOutParameterRegistrationTest.java │ │ ├── NumberedValueExtractorResultExtractorTest.java │ │ ├── ProcedureCallerTest.java │ │ ├── FetchSizeTest.java │ │ ├── ByIndexAndTypeNameOutParameterRegistrationTest.java │ │ ├── OracleTest.java │ │ ├── CompositeFactoryTest.java │ │ ├── CompositeResourceTest.java │ │ ├── H2Test.java │ │ └── PostgresTest.java └── main │ ├── java │ └── com │ │ └── github │ │ └── marschall │ │ └── storedprocedureproxy │ │ ├── spi │ │ ├── package-info.java │ │ ├── Prefix.java │ │ ├── WithoutFirst.java │ │ ├── LowerCase.java │ │ ├── UpperCase.java │ │ ├── TypeNameResolver.java │ │ ├── Capitalize.java │ │ ├── Compund.java │ │ ├── SnakeCase.java │ │ ├── TypeMapper.java │ │ └── NamingStrategy.java │ │ ├── annotations │ │ ├── package-info.java │ │ ├── ProcedureName.java │ │ ├── TypeName.java │ │ ├── ParameterType.java │ │ ├── Schema.java │ │ ├── ParameterName.java │ │ ├── Namespace.java │ │ ├── FetchSize.java │ │ ├── InOutParameter.java │ │ ├── ReturnValue.java │ │ └── OutParameter.java │ │ ├── DefaultMethodSupport.java │ │ ├── IncorrectResultSizeExceptionGenerator.java │ │ ├── DefaultMethodSupportFactory.java │ │ ├── DefaultIncorrectResultSizeExceptionGenerator.java │ │ ├── NoDefaultMethodSupport.java │ │ ├── SpringIncorrectResultSizeExceptionGenerator.java │ │ ├── IncorrectResultSizeException.java │ │ ├── UncheckedSQLExceptionAdapter.java │ │ ├── ByteUtils.java │ │ ├── DelegatingTypeNameResolver.java │ │ ├── ValueExtractorUtils.java │ │ ├── SQLExceptionAdapter.java │ │ ├── UncheckedSQLException.java │ │ ├── OracleTypeMapper.java │ │ ├── ValueExtractor.java │ │ ├── SpringSQLExceptionAdapter.java │ │ ├── NumberedValueExtractor.java │ │ ├── ToStringUtils.java │ │ ├── AnnotationBasedTypeNameResolver.java │ │ ├── DefaultTypeMapper.java │ │ └── DefaultTypeNameResolver.java │ ├── java16 │ └── com │ │ └── github │ │ └── marschall │ │ └── storedprocedureproxy │ │ ├── DefaultMethodSupportFactory.java │ │ └── Java16DefaultMethodSupport.java │ ├── java9 │ ├── com │ │ └── github │ │ │ └── marschall │ │ │ └── storedprocedureproxy │ │ │ ├── DefaultMethodSupportFactory.java │ │ │ └── Java9DefaultMethodSupport.java │ └── module-info.java │ └── resources │ └── LICENSE ├── .travis.yml ├── UNSURE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | release.properties 3 | pom.xml.releaseBackup 4 | 5 | -------------------------------------------------------------------------------- /hacking.md: -------------------------------------------------------------------------------- 1 | Postgres 2 | - registerOutParameter int, int, String -> ignore String 3 | - defer Array string creation 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/test/resources/oracle/01_users.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER SESSION SET CONTAINER = FREEPDB1; 3 | 4 | CREATE USER jdbc IDENTIFIED BY "Cent-Quick-Space-Bath-8"; 5 | -------------------------------------------------------------------------------- /src/test/resources/mariadb/01_permissions.sql: -------------------------------------------------------------------------------- 1 | 2 | -- likely bug as we are the owner https://jira.mariadb.org/browse/MDEV-18554 3 | 4 | GRANT SELECT ON `mysql`.`proc` TO 'jdbc'@'%'; 5 | -------------------------------------------------------------------------------- /src/test/resources/oracle/02_permissions.sql: -------------------------------------------------------------------------------- 1 | 2 | ALTER SESSION SET CONTAINER = FREEPDB1; 3 | 4 | GRANT CONNECT TO jdbc CONTAINER=CURRENT; 5 | GRANT CREATE SESSION TO jdbc CONTAINER=CURRENT; 6 | GRANT RESOURCE TO jdbc CONTAINER=CURRENT; 7 | 8 | ALTER USER jdbc QUOTA 100M ON USERS; 9 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/spi/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains interfaces that may be implemented by users to customize 3 | * functionality if the default behavior is not appropriate for their 4 | * use case. 5 | */ 6 | package com.github.marschall.storedprocedureproxy.spi; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Use docker-based build environment (instead of openvz) 2 | sudo: false 3 | 4 | language: java 5 | 6 | jdk: 7 | - openjdk8 8 | - openjdk11 9 | - openjdk17 10 | 11 | services: 12 | - mysql 13 | - postgresql 14 | 15 | cache: 16 | directories: 17 | - '$HOME/.m2/repository' 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/annotations/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains annotations to that provide additional information about a 3 | * stored procedure in addition of what can be deduced from a Java 4 | * method declaration. 5 | */ 6 | package com.github.marschall.storedprocedureproxy.annotations; -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/DefaultMethodSupport.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | interface DefaultMethodSupport { 6 | 7 | Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/IncorrectResultSizeExceptionGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | @FunctionalInterface 4 | interface IncorrectResultSizeExceptionGenerator { 5 | 6 | RuntimeException newIncorrectResultSizeException(int expectedSize, int actualSize); 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/test/resources/run_postgres.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # https://pythonspeed.com/articles/faster-db-tests/ 3 | docker run --name jdbc-postgres \ 4 | -e 'POSTGRES_PASSWORD=Cent-Quick-Space-Bath-8' \ 5 | -e POSTGRES_USER=$USER \ 6 | -p 5432:5432 \ 7 | --mount type=tmpfs,destination=/var/lib/postgresql/data \ 8 | -d postgres:15.4-alpine 9 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/DefaultMethodSupportFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | final class DefaultMethodSupportFactory { 4 | 5 | static DefaultMethodSupport newInstance(Class interfaceDeclaration) { 6 | return NoDefaultMethodSupport.INSTANCE; 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java16/com/github/marschall/storedprocedureproxy/DefaultMethodSupportFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | final class DefaultMethodSupportFactory { 4 | 5 | static DefaultMethodSupport newInstance(Class interfaceDeclaration) { 6 | return Java16DefaultMethodSupport.INSTANCE; 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java9/com/github/marschall/storedprocedureproxy/DefaultMethodSupportFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | final class DefaultMethodSupportFactory { 4 | 5 | static DefaultMethodSupport newInstance(Class interfaceDeclaration) { 6 | return new Java9DefaultMethodSupport(interfaceDeclaration); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/Travis.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | public final class Travis { 4 | 5 | private Travis() { 6 | throw new AssertionError("not instantiable"); 7 | } 8 | 9 | public static boolean isTravis() { 10 | return System.getenv().getOrDefault("TRAVIS", "false").equals("true"); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/sql/db2_procedures.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION sales_tax(subtotal real) 2 | RETURNS real 3 | LANGUAGE SQL 4 | DETERMINISTIC 5 | BEGIN 6 | RETURN subtotal * 0.06; 7 | END 8 | / 9 | 10 | CREATE OR REPLACE PROCEDURE property_tax(IN subtotal real, OUT tax real) 11 | LANGUAGE SQL 12 | DETERMINISTIC 13 | BEGIN 14 | SET tax = subtotal * 0.06; 15 | END 16 | / 17 | 18 | -------------------------------------------------------------------------------- /src/test/resources/run_mysql.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIRECTORY=`dirname $0` 4 | DIRECTORY=$(realpath $DIRECTORY) 5 | 6 | docker run --name jdbc-mysql \ 7 | -e MYSQL_ROOT_PASSWORD=$USER \ 8 | -e MYSQL_USER=$USER \ 9 | -e MYSQL_PASSWORD=$USER \ 10 | -e MYSQL_DATABASE=$USER \ 11 | -p 3306:3306 \ 12 | --mount type=tmpfs,destination=/var/lib/mysql \ 13 | -d mysql:8.1.0 \ 14 | --log-bin-trust-function-creators=1 15 | -------------------------------------------------------------------------------- /src/test/resources/run_sql_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # https://docs.microsoft.com/en-us/sql/linux/sql-server-linux-setup-docker 3 | # https://github.com/Microsoft/mssql-docker/issues/110 4 | # --mount type=tmpfs,destination=/var/opt/mssql 5 | docker run --name jdbc-sqlserver \ 6 | -e 'ACCEPT_EULA=Y' \ 7 | -e 'SA_PASSWORD=Cent-Quick-Space-Bath-8' \ 8 | -p 1433:1433 \ 9 | -d mcr.microsoft.com/mssql/server:2022-latest 10 | -------------------------------------------------------------------------------- /src/test/resources/log4j2-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/resources/run_db2.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # https://hub.docker.com/r/ibmcom/db2 3 | # -v :/database 4 | DIRECTORY=`dirname $0` 5 | DIRECTORY=$(realpath $DIRECTORY) 6 | 7 | docker run -itd --name jdbc-db2 \ 8 | -p 50000:50000 \ 9 | --privileged=true \ 10 | -e LICENSE=accept \ 11 | -e 'DB2INST1_PASSWORD=Cent-Quick-Space-Bath-8' \ 12 | -e DBNAME=jdbc \ 13 | -e ARCHIVE_LOGS=false \ 14 | ibmcom/db2:11.5.7.0 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/spi/Prefix.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.spi; 2 | 3 | final class Prefix implements NamingStrategy { 4 | 5 | private final String prefix; 6 | 7 | Prefix(String prefix) { 8 | this.prefix = prefix; 9 | } 10 | 11 | @Override 12 | public String translateToDatabase(String javaName) { 13 | return this.prefix + javaName; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/spi/WithoutFirst.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.spi; 2 | 3 | final class WithoutFirst implements NamingStrategy { 4 | 5 | private final int skipped; 6 | 7 | WithoutFirst(int skipped) { 8 | this.skipped = skipped; 9 | } 10 | 11 | @Override 12 | public String translateToDatabase(String javaName) { 13 | return javaName.substring(skipped); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/run_firebird.sh: -------------------------------------------------------------------------------- 1 | # https://hub.docker.com/r/jacobalberty/firebird/ 2 | # enter container console 3 | # docker exec -i -t firebird3 /bin/bash 4 | # -e 'ISC_PASSWORD=masterkey' \ 5 | 6 | docker run --name jdbc-firebird \ 7 | -e 'FIREBIRD_DATABASE=jdbc' \ 8 | -e 'FIREBIRD_USER=jdbc' \ 9 | -e 'FIREBIRD_PASSWORD=Cent-Quick-Space-Bath-8' \ 10 | -p 3050:3050 \ 11 | --mount type=tmpfs,destination=/firebird/data \ 12 | -d jacobalberty/firebird:v4.0.1 -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/NoResourceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class NoResourceTest { 8 | 9 | @Test 10 | public void testToString() { 11 | CallResource resource = NoResource.INSTANCE; 12 | assertEquals("NoResource", resource.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/run_mariadb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DIRECTORY=`dirname $0` 3 | DIRECTORY=$(realpath $DIRECTORY) 4 | 5 | docker run --name jdbc-mariadb \ 6 | -e 'MYSQL_ROOT_PASSWORD=Cent-Quick-Space-Bath-8' \ 7 | -e 'MYSQL_USER=jdbc' \ 8 | -e 'MYSQL_PASSWORD=Cent-Quick-Space-Bath-8' \ 9 | -e 'MYSQL_DATABASE=jdbc' \ 10 | -p 3307:3306 \ 11 | --mount type=tmpfs,destination=/var/lib/mysql \ 12 | -v ${DIRECTORY}/mariadb:/docker-entrypoint-initdb.d \ 13 | -d mariadb:11.0.3 14 | -------------------------------------------------------------------------------- /src/main/java9/module-info.java: -------------------------------------------------------------------------------- 1 | 2 | module com.github.marschall.stored.procedure.proxy { 3 | 4 | exports com.github.marschall.storedprocedureproxy; 5 | exports com.github.marschall.storedprocedureproxy.annotations; 6 | exports com.github.marschall.storedprocedureproxy.spi; 7 | 8 | requires static spring.beans; 9 | requires static spring.core; 10 | requires static spring.jdbc; 11 | requires static spring.tx; 12 | requires static org.postgresql.jdbc; 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/resources/run_oracle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # https://github.com/oracle/docker-images/blob/master/OracleDatabase/SingleInstance/README.md#running-oracle-database-enterprise-and-standard-edition-2-in-a-docker-container 3 | DIRECTORY=`dirname $0` 4 | DIRECTORY=$(realpath $DIRECTORY) 5 | 6 | docker run --name jdbc-oracle \ 7 | -p 1521:1521 -p 5500:5500 \ 8 | --shm-size=1g \ 9 | -v ${DIRECTORY}/oracle:/docker-entrypoint-initdb.d/setup \ 10 | -d oracle/database:23.2.0-free 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/DefaultIncorrectResultSizeExceptionGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | final class DefaultIncorrectResultSizeExceptionGenerator implements IncorrectResultSizeExceptionGenerator { 4 | 5 | @Override 6 | public RuntimeException newIncorrectResultSizeException(int expectedSize, int actualSize) { 7 | return new IncorrectResultSizeException(expectedSize, actualSize); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/NoResourceFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class NoResourceFactoryTest { 8 | 9 | @Test 10 | public void testToString() { 11 | CallResourceFactory factory = NoResourceFactory.INSTANCE; 12 | assertEquals("NoResourceFactory", factory.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/spi/LowerCase.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.spi; 2 | 3 | import java.util.Locale; 4 | 5 | final class LowerCase implements NamingStrategy { 6 | 7 | static final NamingStrategy INSTANCE = new LowerCase(); 8 | 9 | private LowerCase() { 10 | super(); 11 | } 12 | 13 | @Override 14 | public String translateToDatabase(String javaName) { 15 | return javaName.toLowerCase(Locale.US); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/spi/UpperCase.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.spi; 2 | 3 | import java.util.Locale; 4 | 5 | final class UpperCase implements NamingStrategy { 6 | 7 | static final NamingStrategy INSTANCE = new UpperCase(); 8 | 9 | private UpperCase() { 10 | super(); 11 | } 12 | 13 | @Override 14 | public String translateToDatabase(String javaName) { 15 | return javaName.toUpperCase(Locale.US); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/VoidResultExtractorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class VoidResultExtractorTest { 8 | 9 | @Test 10 | public void testToString() { 11 | ResultExtractor extractor = VoidResultExtractor.INSTANCE; 12 | assertEquals("VoidResultExtractor", extractor.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/JavaVersionSupport.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | final class JavaVersionSupport { 4 | 5 | private JavaVersionSupport() { 6 | throw new AssertionError("not instantiable"); 7 | } 8 | 9 | static boolean isJava9OrLater() { 10 | try { 11 | Class.forName("java.lang.Runtime$Version"); 12 | return true; 13 | } catch (ClassNotFoundException e) { 14 | return false; 15 | } 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ArrayFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class ArrayFactoryTest { 8 | 9 | @Test 10 | public void testToString() { 11 | CallResourceFactory factory = new ArrayFactory(1, "INTEGER"); 12 | assertEquals("ArrayFactory[argumentIndex=1, typeName=INTEGER]", factory.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/sql/mariadb_procedures.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP FUNCTION IF EXISTS hello_function; 3 | CREATE FUNCTION hello_function (s CHAR(20)) 4 | RETURNS CHAR(50) DETERMINISTIC 5 | RETURN CONCAT('Hello, ',s,'!'); 6 | 7 | DROP PROCEDURE IF EXISTS hello_procedure; 8 | CREATE PROCEDURE hello_procedure (IN s CHAR(20), OUT result VARCHAR(100)) 9 | SET result = CONCAT('Hello, ',s,'!'); 10 | 11 | DROP PROCEDURE IF EXISTS fake_refcursor; 12 | CREATE PROCEDURE fake_refcursor () 13 | SELECT 'hello' UNION ALL SELECT 'mysql'; 14 | 15 | -------------------------------------------------------------------------------- /src/test/resources/sql/mysql_procedures.sql: -------------------------------------------------------------------------------- 1 | 2 | DROP FUNCTION IF EXISTS hello_function; 3 | CREATE FUNCTION hello_function (s CHAR(20)) 4 | RETURNS CHAR(50) DETERMINISTIC 5 | RETURN CONCAT('Hello, ',s,'!'); 6 | 7 | DROP PROCEDURE IF EXISTS hello_procedure; 8 | CREATE PROCEDURE hello_procedure (IN s CHAR(20), OUT result VARCHAR(100)) 9 | SET result = CONCAT('Hello, ',s,'!'); 10 | 11 | DROP PROCEDURE IF EXISTS fake_refcursor; 12 | CREATE PROCEDURE fake_refcursor () 13 | SELECT 'hello' UNION ALL SELECT 'mysql'; 14 | 15 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ScalarResultExtractorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class ScalarResultExtractorTest { 8 | 9 | @Test 10 | public void testToString() { 11 | ResultExtractor extractor = new ScalarResultExtractor(Integer.class); 12 | assertEquals("ScalarResultExtractor[Integer]", extractor.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/NoInParameterRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class NoInParameterRegistrationTest { 8 | 9 | @Test 10 | public void testToString() { 11 | InParameterRegistration registration = NoInParameterRegistration.INSTANCE; 12 | assertEquals("NoInParameterRegistration", registration.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/OracleArrayFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class OracleArrayFactoryTest { 8 | 9 | @Test 10 | public void testToString() { 11 | CallResourceFactory factory = new OracleArrayFactory(1, "INTEGER"); 12 | assertEquals("OracleArrayFactory[argumentIndex=1, typeName=INTEGER]", factory.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/NoOutParameterRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class NoOutParameterRegistrationTest { 8 | 9 | @Test 10 | public void testToString() { 11 | OutParameterRegistration registration = NoOutParameterRegistration.INSTANCE; 12 | assertEquals("NoOutParameterRegistration", registration.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/OracleArrayResultExtractorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class OracleArrayResultExtractorTest { 8 | 9 | @Test 10 | public void testToString() { 11 | ResultExtractor extractor = new OracleArrayResultExtractor(Integer.class); 12 | assertEquals("OracleArrayResultExtractor[Integer]", extractor.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/sql/mssql_procedures.sql: -------------------------------------------------------------------------------- 1 | DROP PROCEDURE IF EXISTS plus1inout; 2 | 3 | CREATE PROCEDURE plus1inout 4 | @arg INT, 5 | @res INT OUTPUT 6 | AS 7 | BEGIN 8 | SET @res = @arg + 1 9 | END; 10 | 11 | DROP FUNCTION IF EXISTS plus1inret; 12 | 13 | CREATE FUNCTION plus1inret(@arg int) 14 | RETURNS int 15 | AS 16 | BEGIN 17 | RETURN @arg + 1 18 | END; 19 | 20 | DROP PROCEDURE IF EXISTS fakeCursor; 21 | 22 | CREATE PROCEDURE fakeCursor 23 | AS 24 | BEGIN 25 | SELECT 'hello' UNION ALL SELECT 'world' 26 | END; 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/NoDefaultMethodSupport.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | final class NoDefaultMethodSupport implements DefaultMethodSupport { 6 | 7 | static final DefaultMethodSupport INSTANCE = new NoDefaultMethodSupport(); 8 | 9 | @Override 10 | public Object invokeDefaultMethod(Object proxy, Method method, Object[] args) { 11 | throw new IllegalStateException("default methods are not only supported in Java 9 or later"); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/procedures/MssqlProcedures.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.procedures; 2 | 3 | import java.util.List; 4 | 5 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 6 | import com.github.marschall.storedprocedureproxy.annotations.ReturnValue; 7 | 8 | public interface MssqlProcedures { 9 | 10 | @OutParameter(name = "res") 11 | int plus1inout(int arg); 12 | 13 | @ReturnValue 14 | int plus1inret(int arg); 15 | 16 | List fakeCursor(); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/SpringIncorrectResultSizeExceptionGenerator.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import org.springframework.dao.IncorrectResultSizeDataAccessException; 4 | 5 | final class SpringIncorrectResultSizeExceptionGenerator implements IncorrectResultSizeExceptionGenerator { 6 | 7 | @Override 8 | public RuntimeException newIncorrectResultSizeException(int expectedSize, int actualSize) { 9 | return new IncorrectResultSizeDataAccessException(expectedSize, actualSize); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ByIndexInParameterRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class ByIndexInParameterRegistrationTest { 8 | 9 | @Test 10 | public void testToString() { 11 | InParameterRegistration registration = new ByIndexInParameterRegistration(new byte[] {1, -128, -1}); 12 | assertEquals("ByIndexInParameterRegistration[1, 128, 255]", registration.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ByNameInParameterRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class ByNameInParameterRegistrationTest { 8 | 9 | @Test 10 | public void testToString() { 11 | InParameterRegistration registration = new ByNameInParameterRegistration(new String[] {"dog", "cat"}); 12 | assertEquals("ByNameInParameterRegistration[dog, cat]", registration.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/PrefixByIndexInParameterRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class PrefixByIndexInParameterRegistrationTest { 8 | 9 | @Test 10 | public void testToString() { 11 | InParameterRegistration registration = PrefixByIndexInParameterRegistration.INSTANCE; 12 | assertEquals("PrefixByIndexInParameterRegistration[i + 2]", registration.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/SuffixByIndexInParameterRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class SuffixByIndexInParameterRegistrationTest { 8 | 9 | @Test 10 | public void testToString() { 11 | InParameterRegistration registration = SuffixByIndexInParameterRegistration.INSTANCE; 12 | assertEquals("SuffixByIndexInParameterRegistration[i + 1]", registration.toString()); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/DerbyProcedureSources.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public class DerbyProcedureSources { 6 | 7 | public static void calculateRevenueByMonth(int month, int year, BigDecimal[] out) { 8 | out[0] = new BigDecimal(year * 100 + month); 9 | } 10 | 11 | public static double tax(double subTotal) { 12 | return subTotal * 0.06d; 13 | } 14 | 15 | public static void raisePrice(BigDecimal[] price) { 16 | price[0] = price[0].multiply(BigDecimal.valueOf(2L)); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/resources/sql/firebird_procedures.sql: -------------------------------------------------------------------------------- 1 | CREATE OR ALTER PROCEDURE increment(y INTEGER) 2 | RETURNS( x INTEGER) 3 | AS 4 | BEGIN 5 | x = y + 1; 6 | SUSPEND; 7 | END^ 8 | 9 | CREATE PROCEDURE factorial(max_value INTEGER) 10 | RETURNS (factorial INTEGER) 11 | AS 12 | DECLARE VARIABLE temp INTEGER; 13 | DECLARE VARIABLE row_num INTEGER; 14 | BEGIN 15 | row_num = 0; 16 | WHILE (row_num <= max_value) DO BEGIN 17 | IF (row_num <= 1) THEN 18 | temp = 1; 19 | ELSE 20 | temp = temp * row_num; 21 | factorial = temp; 22 | row_num = row_num + 1; 23 | SUSPEND; 24 | END 25 | END^ 26 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ByIndexOutParameterRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.sql.Types; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class ByIndexOutParameterRegistrationTest { 10 | 11 | @Test 12 | public void testToString() { 13 | OutParameterRegistration registration = new ByIndexOutParameterRegistration(254, Types.INTEGER); 14 | assertEquals("ByIndexOutParameterRegistration[index=254, type=4]", registration.toString()); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ByNameOutParameterRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.sql.Types; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class ByNameOutParameterRegistrationTest { 10 | 11 | @Test 12 | public void testToString() { 13 | OutParameterRegistration registration = new ByNameOutParameterRegistration("dog", Types.INTEGER); 14 | assertEquals("ByNameOutParameterRegistration[name=dog, type=4]", registration.toString()); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/IncorrectResultSizeException.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | 4 | /** 5 | * Signals an unexpected amount of rows was returned. 6 | * 7 | * @see org.springframework.dao.IncorrectResultSizeDataAccessException 8 | */ 9 | public final class IncorrectResultSizeException extends RuntimeException { 10 | 11 | private static final long serialVersionUID = 1L; 12 | 13 | IncorrectResultSizeException(int expectedSize, int actualSize) { 14 | super("Incorrect result size: expected " + expectedSize + ", but was " + actualSize); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /UNSURE.md: -------------------------------------------------------------------------------- 1 | 2 | Unsure 3 | ------ 4 | - Is there a better way to avoid having `@OutParameter` or `@ReturnValue` 5 | - Should we support other collections than list? 6 | - Out parameter default last 7 | - name of the SPI package 8 | - type annotation for element in ref cursor 9 | - rename `@OutParameter` to `@Procedure`, `@ReturnValue` to `@Function` 10 | - combine `@OutParameter` and `@Function` and remove attirbutes from `@Function`? 11 | - SQL Arrays as Lists 12 | - SQL Arrays with Value extractors 13 | - ref cursors as Java arrays 14 | - @TypeName for array return types 15 | - should @InOutParameter be on an argument instead of the method 16 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/spi/TypeNameResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.spi; 2 | 3 | import java.lang.reflect.Parameter; 4 | import java.sql.Connection; 5 | 6 | /** 7 | * Resolves the SQL name of a type. 8 | * 9 | * @see Connection#createArrayOf(String, Object[]) 10 | */ 11 | @FunctionalInterface 12 | public interface TypeNameResolver { 13 | 14 | /** 15 | * Resolve the SQL name of a type. 16 | * 17 | * @param parameter the method parameter who's type 18 | * @return the SQL name of the type 19 | */ 20 | String resolveTypeName(Parameter parameter); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/procedures/Db2Procedures.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.procedures; 2 | 3 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 4 | import com.github.marschall.storedprocedureproxy.annotations.ProcedureName; 5 | import com.github.marschall.storedprocedureproxy.annotations.ReturnValue; 6 | 7 | public interface Db2Procedures { 8 | 9 | @ProcedureName("sales_tax") 10 | @ReturnValue 11 | float salesTax(float subtotal); 12 | 13 | @ProcedureName("property_tax") 14 | @OutParameter(name = "tax") 15 | float propertyTax(float subtotal); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/procedures/OracleProcedures.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.procedures; 2 | 3 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 4 | import com.github.marschall.storedprocedureproxy.annotations.ProcedureName; 5 | import com.github.marschall.storedprocedureproxy.annotations.ReturnValue; 6 | 7 | public interface OracleProcedures { 8 | 9 | @ProcedureName("sales_tax") 10 | @ReturnValue 11 | float salesTax(float subtotal); 12 | 13 | @ProcedureName("property_tax") 14 | @OutParameter(name = "tax") 15 | float propertyTax(float subtotal); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ArrayResourceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.mockito.Mockito.mock; 5 | 6 | import java.sql.Array; 7 | import java.sql.SQLException; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class ArrayResourceTest { 12 | 13 | @Test 14 | public void testToString() throws SQLException { 15 | Array array = mock(Array.class); 16 | try (CallResource resource = new ArrayResource(array, 1)) { 17 | assertEquals("ArrayResource[1]", resource.toString()); 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/DisabledOnTravis.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static java.lang.annotation.ElementType.TYPE; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | 12 | /** 13 | * Prevents a test from running on travis. 14 | */ 15 | @Documented 16 | @Retention(RUNTIME) 17 | @Target(TYPE) 18 | @ExtendWith(DisabledOnTravisCondition.class) 19 | public @interface DisabledOnTravis { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/procedures/HsqlProcedures.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.procedures; 2 | 3 | import java.sql.Timestamp; 4 | import java.util.List; 5 | 6 | import com.github.marschall.storedprocedureproxy.annotations.ProcedureName; 7 | 8 | public interface HsqlProcedures { 9 | 10 | @ProcedureName("an_hour_before") 11 | Timestamp anHourBefore(Timestamp t); 12 | 13 | @ProcedureName("one_two") 14 | List refCursor(); 15 | 16 | @ProcedureName("array_cardinality") 17 | int arrayCardinality(Integer[] integer); 18 | 19 | @ProcedureName("return_array") 20 | Integer[] returnArray(); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/resources/sql/hsql_procedures.sql: -------------------------------------------------------------------------------- 1 | CREATE PROCEDURE plus1inout (IN arg int, OUT res int) 2 | BEGIN ATOMIC 3 | SET res = arg + 1; 4 | END 5 | /; 6 | 7 | CREATE FUNCTION an_hour_before (t TIMESTAMP) 8 | RETURNS TIMESTAMP 9 | RETURN t - 1 HOUR; 10 | /; 11 | 12 | CREATE FUNCTION array_cardinality (a INT ARRAY) 13 | RETURNS INT 14 | RETURN CARDINALITY(a); 15 | /; 16 | 17 | CREATE FUNCTION return_array () 18 | RETURNS INT ARRAY 19 | RETURN SEQUENCE_ARRAY(0, 10, 5); 20 | /; 21 | 22 | CREATE FUNCTION one_two() 23 | RETURNS TABLE(id integer) 24 | READS SQL DATA BEGIN ATOMIC 25 | RETURN TABLE(SELECT 1 FROM (VALUES(0)) UNION ALL SELECT 2 FROM (VALUES(0))); 26 | END 27 | /; 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ByNameAndTypeInParameterRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.sql.Types; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class ByNameAndTypeInParameterRegistrationTest { 10 | 11 | @Test 12 | public void testToString() { 13 | InParameterRegistration registration = new ByNameAndTypeInParameterRegistration( 14 | new String[] {"dog", "cat"}, new int[] {Types.VARCHAR, Types.INTEGER}); 15 | assertEquals("ByNameAndTypeInParameterRegistration[names={dog, cat}, types={12, 4}]", registration.toString()); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ByIndexAndTypeInParameterRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.sql.Types; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class ByIndexAndTypeInParameterRegistrationTest { 10 | 11 | @Test 12 | public void testToString() { 13 | InParameterRegistration registration = new ByIndexAndTypeInParameterRegistration( 14 | new byte[] {1, -128, -1}, new int[] {Types.VARCHAR, Types.INTEGER, Types.TIMESTAMP}); 15 | assertEquals("ByIndexAndTypeInParameterRegistration[indexes={1, 128, 255}, types={12, 4, 93}]", registration.toString()); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/AbstractDataSourceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import javax.sql.DataSource; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | import com.github.marschall.storedprocedureproxy.configuration.TestConfiguration; 10 | 11 | @Transactional 12 | @SpringJUnitConfig(TestConfiguration.class) 13 | public abstract class AbstractDataSourceTest { 14 | 15 | @Autowired 16 | private DataSource dataSource; 17 | 18 | protected DataSource getDataSource() { 19 | return dataSource; 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/AllParametersRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static java.lang.annotation.ElementType.METHOD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.EnumSource; 11 | 12 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 13 | 14 | @Retention(RUNTIME) 15 | @Target(METHOD) 16 | @ParameterizedTest 17 | @EnumSource(ParameterRegistration.class) 18 | public @interface AllParametersRegistrationTest { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/resources/sql/derby_procedures.sql: -------------------------------------------------------------------------------- 1 | CREATE PROCEDURE SALES.TOTAL_REVENUE(IN s_month INTEGER, IN s_year INTEGER, OUT total DECIMAL(10,2)) 2 | PARAMETER STYLE JAVA READS SQL DATA LANGUAGE JAVA 3 | EXTERNAL NAME 'com.github.marschall.storedprocedureproxy.DerbyProcedureSources.calculateRevenueByMonth'; 4 | 5 | CREATE FUNCTION SALES.TAX(SUBTOTAL DOUBLE) RETURNS DOUBLE 6 | PARAMETER STYLE JAVA 7 | NO SQL 8 | LANGUAGE JAVA 9 | EXTERNAL NAME 'com.github.marschall.storedprocedureproxy.DerbyProcedureSources.tax'; 10 | 11 | CREATE PROCEDURE SALES.RAISE_PRICE(INOUT newPrice NUMERIC(10,2)) 12 | PARAMETER STYLE JAVA 13 | LANGUAGE JAVA 14 | DYNAMIC RESULT SETS 0 15 | EXTERNAL NAME 'com.github.marschall.storedprocedureproxy.DerbyProcedureSources.raisePrice'; 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/test/resources/sql/h2_procedures.sql: -------------------------------------------------------------------------------- 1 | CREATE ALIAS STRING_PROCEDURE FOR "com.github.marschall.storedprocedureproxy.H2ProcedureSources.stringProcedure"; 2 | 3 | CREATE ALIAS VOID_PROCEDURE FOR "com.github.marschall.storedprocedureproxy.H2ProcedureSources.voidProcedure"; 4 | 5 | CREATE ALIAS NO_ARG_PROCEDURE FOR "com.github.marschall.storedprocedureproxy.H2ProcedureSources.noArgProcedure"; 6 | 7 | CREATE ALIAS SIMPLE_RESULT_SET FOR "com.github.marschall.storedprocedureproxy.H2ProcedureSources.simpleResultSet"; 8 | 9 | CREATE ALIAS REVERSE_INTEGER_ARRAY FOR "com.github.marschall.storedprocedureproxy.H2ProcedureSources.reverseIntegerArray"; 10 | 11 | CREATE ALIAS RETURN_INTEGER_ARRAY FOR "com.github.marschall.storedprocedureproxy.H2ProcedureSources.returnIntegerArray"; 12 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/procedures/FirebirdProcedures.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.procedures; 2 | 3 | import java.util.List; 4 | 5 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 6 | import com.github.marschall.storedprocedureproxy.annotations.ProcedureName; 7 | import com.github.marschall.storedprocedureproxy.annotations.ReturnValue; 8 | 9 | public interface FirebirdProcedures { 10 | 11 | int increment(int y); 12 | 13 | @OutParameter(name = "x") 14 | @ProcedureName("increment") 15 | int incrementOutParameter(int y); 16 | 17 | @ReturnValue 18 | @ProcedureName("increment") 19 | int incrementReturnValue(int y); 20 | 21 | List factorial(int maxValue); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/UncheckedSQLExceptionAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.sql.SQLException; 4 | 5 | /** 6 | * A {@link SQLExceptionAdapter} that creates a new {@link UncheckedSQLException}. 7 | */ 8 | final class UncheckedSQLExceptionAdapter implements SQLExceptionAdapter { 9 | 10 | static final SQLExceptionAdapter INSTANCE = new UncheckedSQLExceptionAdapter(); 11 | 12 | private UncheckedSQLExceptionAdapter() { 13 | super(); 14 | } 15 | 16 | @Override 17 | public RuntimeException translate(String procedureName, String sql, SQLException ex) { 18 | return new UncheckedSQLException("failed to call function '" + procedureName + "' with sql: " + sql, ex); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/ByteUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | final class ByteUtils { 4 | 5 | private ByteUtils() { 6 | throw new AssertionError("not instantiable"); 7 | } 8 | 9 | static int toInt(byte b) { 10 | return Byte.toUnsignedInt(b); 11 | } 12 | 13 | static byte toByte(int i) { 14 | if (i < 0 || i > 255) { 15 | throw new IllegalArgumentException(); 16 | } 17 | return (byte) i; 18 | } 19 | 20 | static void toStringOn(byte[] array, StringBuilder builder) { 21 | for (int i = 0; i < array.length; i++) { 22 | if (i > 0) { 23 | builder.append(", "); 24 | } 25 | int element = toInt(array[i]); 26 | builder.append(element); 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/IndexedParametersRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static java.lang.annotation.ElementType.METHOD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.EnumSource; 11 | 12 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 13 | 14 | @Retention(RUNTIME) 15 | @Target(METHOD) 16 | @ParameterizedTest 17 | @EnumSource(value = ParameterRegistration.class, names = {"INDEX_ONLY", "INDEX_AND_TYPE"}) 18 | public @interface IndexedParametersRegistrationTest { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/configuration/TestConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.configuration; 2 | 3 | import javax.sql.DataSource; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.jdbc.datasource.DataSourceTransactionManager; 9 | import org.springframework.transaction.PlatformTransactionManager; 10 | 11 | @Configuration 12 | public class TestConfiguration { 13 | 14 | @Autowired 15 | private DataSource dataSource; 16 | 17 | @Bean 18 | public PlatformTransactionManager txManager() { 19 | return new DataSourceTransactionManager(this.dataSource); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ListResultExtractorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ProcedureCaller; 8 | 9 | public class ListResultExtractorTest { 10 | 11 | @Test 12 | public void testToString() { 13 | ResultExtractor extractor = new ListResultExtractor(Integer.class, ProcedureCaller.DEFAULT_FETCH_SIZE); 14 | assertEquals("ListResultExtractor[type=Integer, fetchSize=default]", extractor.toString()); 15 | 16 | extractor = new ListResultExtractor(Integer.class, 10); 17 | assertEquals("ListResultExtractor[type=Integer, fetchSize=10]", extractor.toString()); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/DelegatingTypeNameResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.lang.reflect.Parameter; 4 | 5 | import com.github.marschall.storedprocedureproxy.spi.TypeNameResolver; 6 | 7 | final class DelegatingTypeNameResolver implements TypeNameResolver { 8 | 9 | private final TypeNameResolver delegate; 10 | 11 | DelegatingTypeNameResolver(TypeNameResolver delegate) { 12 | this.delegate = delegate; 13 | } 14 | 15 | @Override 16 | public String resolveTypeName(Parameter parameter) { 17 | String typeName = AnnotationBasedTypeNameResolver.INSTANCE.resolveTypeName(parameter); 18 | if (typeName != null) { 19 | return typeName; 20 | } else { 21 | return this.delegate.resolveTypeName(parameter); 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/annotations/ProcedureName.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.annotations; 2 | 3 | import static java.lang.annotation.ElementType.METHOD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * Defines the name of a stored procedure. 12 | * 13 | * @see Deriving Names 14 | */ 15 | @Documented 16 | @Retention(RUNTIME) 17 | @Target(METHOD) 18 | public @interface ProcedureName { 19 | 20 | /** 21 | * Defines the name of the stored procedure. 22 | * 23 | * @return the name of the stored procedure 24 | */ 25 | String value(); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ByteUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | public class ByteUtilsTest { 9 | 10 | @Test 11 | public void byteIntConversions() { 12 | assertInt(0); 13 | assertInt(127); 14 | assertInt(128); 15 | assertInt(254); 16 | } 17 | 18 | @Test 19 | public void invalidConversions() { 20 | assertThrows(IllegalArgumentException.class, () -> ByteUtils.toByte(-1)); 21 | 22 | assertThrows(IllegalArgumentException.class, () -> ByteUtils.toByte(256)); 23 | } 24 | 25 | private static void assertInt(int i) { 26 | assertEquals(i, ByteUtils.toInt(ByteUtils.toByte(i))); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/ValueExtractorUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | final class ValueExtractorUtils { 4 | 5 | private ValueExtractorUtils() { 6 | throw new AssertionError("not instantiable"); 7 | } 8 | 9 | static boolean isAnyValueExtractor(Class clazz) { 10 | return isValueExtractor(clazz) 11 | // || isFunction(clazz) 12 | || isNumberedValueExtractor(clazz); 13 | } 14 | 15 | static boolean isValueExtractor(Class clazz) { 16 | return clazz.isAssignableFrom(ValueExtractor.class); 17 | } 18 | 19 | static boolean isNumberedValueExtractor(Class clazz) { 20 | return clazz.isAssignableFrom(NumberedValueExtractor.class); 21 | } 22 | 23 | // static boolean isFunction(Class clazz) { 24 | // return clazz.isAssignableFrom(Function.class); 25 | // } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/procedures/MysqlProcedures.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.procedures; 2 | 3 | import java.util.List; 4 | 5 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 6 | import com.github.marschall.storedprocedureproxy.annotations.ParameterName; 7 | import com.github.marschall.storedprocedureproxy.annotations.ProcedureName; 8 | import com.github.marschall.storedprocedureproxy.annotations.ReturnValue; 9 | 10 | public interface MysqlProcedures { 11 | 12 | @ProcedureName("hello_function") 13 | @ReturnValue 14 | String helloFunction(@ParameterName("s") String s); 15 | 16 | @ProcedureName("hello_procedure") 17 | @OutParameter(name = "result") 18 | String helloProcedure(@ParameterName("s") String s); 19 | 20 | @ProcedureName("fake_refcursor") 21 | List fakeRefcursor(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/spi/Capitalize.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.spi; 2 | 3 | final class Capitalize implements NamingStrategy { 4 | 5 | static final NamingStrategy INSTANCE = new Capitalize(); 6 | 7 | private Capitalize() { 8 | super(); 9 | } 10 | 11 | @Override 12 | public String translateToDatabase(String javaName) { 13 | int length = javaName.length(); 14 | if (length == 0) { 15 | return javaName; 16 | } 17 | StringBuilder builder = new StringBuilder(length); 18 | char first = javaName.charAt(0); 19 | if (first >= 'a' && first <= 'z') { 20 | builder.append((char) (first + ('A' - 'a'))); 21 | } else { 22 | builder.append(first); 23 | } 24 | if (length > 1) { 25 | builder.append(javaName, 1, length); 26 | } 27 | return builder.toString(); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/configuration/Db2Configuration.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.configuration; 2 | 3 | import javax.sql.DataSource; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.jdbc.datasource.SingleConnectionDataSource; 8 | 9 | @Configuration 10 | public class Db2Configuration { 11 | 12 | @Bean 13 | public DataSource dataSource() { 14 | com.ibm.db2.jcc.DB2Driver.getMyClassLoader(); 15 | SingleConnectionDataSource dataSource = new SingleConnectionDataSource(); 16 | dataSource.setSuppressClose(true); 17 | dataSource.setUrl("jdbc:db2://localhost:50000/jdbc"); 18 | dataSource.setUsername("db2inst1"); 19 | dataSource.setPassword("Cent-Quick-Space-Bath-8"); 20 | return dataSource; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/procedures/MariaDBProcedures.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.procedures; 2 | 3 | import java.util.List; 4 | 5 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 6 | import com.github.marschall.storedprocedureproxy.annotations.ParameterName; 7 | import com.github.marschall.storedprocedureproxy.annotations.ProcedureName; 8 | import com.github.marschall.storedprocedureproxy.annotations.ReturnValue; 9 | 10 | public interface MariaDBProcedures { 11 | 12 | @ProcedureName("hello_function") 13 | @ReturnValue 14 | String helloFunction(@ParameterName("s") String s); 15 | 16 | @ProcedureName("hello_procedure") 17 | @OutParameter(name = "result") 18 | String helloProcedure(@ParameterName("s") String s); 19 | 20 | @ProcedureName("fake_refcursor") 21 | List fakeRefcursor(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/configuration/H2Configuration.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.configuration; 2 | 3 | import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.H2; 4 | 5 | import javax.sql.DataSource; 6 | 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; 10 | 11 | @Configuration 12 | public class H2Configuration { 13 | 14 | @Bean 15 | public DataSource dataSource() { 16 | return new EmbeddedDatabaseBuilder() 17 | .generateUniqueName(true) 18 | .setType(H2) 19 | .setScriptEncoding("UTF-8") 20 | .ignoreFailedDrops(true) 21 | .addScript("sql/h2_procedures.sql") 22 | .build(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/procedures/SamplePackage.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.procedures; 2 | 3 | import java.sql.Types; 4 | 5 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 6 | import com.github.marschall.storedprocedureproxy.annotations.ParameterName; 7 | import com.github.marschall.storedprocedureproxy.annotations.ParameterType; 8 | import com.github.marschall.storedprocedureproxy.annotations.ProcedureName; 9 | import com.github.marschall.storedprocedureproxy.annotations.Schema; 10 | 11 | @Schema("SAMPLE_NAME") 12 | public interface SamplePackage { 13 | 14 | @ProcedureName("SAMPLE_FUNCTION") 15 | void sampleFunction(String argument); 16 | 17 | @ProcedureName("SAMPLE_PROCEDURE") 18 | @OutParameter 19 | String sampleProcedure(@ParameterType(Types.NUMERIC) @ParameterName("p_arg_in") int argument); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/configuration/DerbyConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.configuration; 2 | 3 | import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.DERBY; 4 | 5 | import javax.sql.DataSource; 6 | 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; 10 | 11 | @Configuration 12 | public class DerbyConfiguration { 13 | 14 | @Bean 15 | public DataSource dataSource() { 16 | return new EmbeddedDatabaseBuilder() 17 | .generateUniqueName(true) 18 | .setType(DERBY) 19 | .setScriptEncoding("UTF-8") 20 | .ignoreFailedDrops(true) 21 | .addScript("sql/derby_procedures.sql") 22 | .build(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/spi/Compund.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.spi; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | final class Compund implements NamingStrategy { 7 | 8 | private final List strategies; 9 | 10 | Compund(NamingStrategy first, NamingStrategy second) { 11 | this.strategies = new ArrayList<>(2); 12 | this.strategies.add(first); 13 | this.strategies.add(second); 14 | } 15 | 16 | @Override 17 | public String translateToDatabase(String javaName) { 18 | String databaseName = javaName; 19 | for (NamingStrategy strategy : this.strategies) { 20 | databaseName = strategy.translateToDatabase(databaseName); 21 | } 22 | return databaseName; 23 | } 24 | 25 | @Override 26 | public NamingStrategy then(NamingStrategy next) { 27 | this.strategies.add(next); 28 | return this; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/configuration/HsqlConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.configuration; 2 | 3 | import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.HSQL; 4 | 5 | import javax.sql.DataSource; 6 | 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; 10 | 11 | @Configuration 12 | public class HsqlConfiguration { 13 | 14 | @Bean 15 | public DataSource dataSource() { 16 | return new EmbeddedDatabaseBuilder() 17 | .generateUniqueName(true) 18 | .setType(HSQL) 19 | .setScriptEncoding("UTF-8") 20 | .ignoreFailedDrops(true) 21 | .setSeparator("/;") 22 | .addScript("sql/hsql_procedures.sql") 23 | .build(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/procedures/DerbyProcedures.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.procedures; 2 | 3 | import java.math.BigDecimal; 4 | 5 | import com.github.marschall.storedprocedureproxy.annotations.InOutParameter; 6 | import com.github.marschall.storedprocedureproxy.annotations.Namespace; 7 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 8 | import com.github.marschall.storedprocedureproxy.annotations.ProcedureName; 9 | import com.github.marschall.storedprocedureproxy.annotations.ReturnValue; 10 | 11 | @Namespace("SALES") 12 | public interface DerbyProcedures { 13 | 14 | @ProcedureName("TOTAL_REVENUE") 15 | @OutParameter 16 | BigDecimal calculateRevenueByMonth(int month, int year); 17 | 18 | @ProcedureName("RAISE_PRICE") 19 | @InOutParameter 20 | BigDecimal raisePrice(BigDecimal price); 21 | 22 | @ProcedureName("TAX") 23 | @ReturnValue 24 | double salesTax(double subTotal); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/annotations/TypeName.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.annotations; 2 | 3 | import static java.lang.annotation.ElementType.PARAMETER; 4 | import static java.lang.annotation.ElementType.TYPE_USE; 5 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.Target; 10 | import java.sql.Connection; 11 | 12 | /** 13 | * Defines the name of a type. Useful for array types. 14 | * 15 | * @see Connection#createArrayOf(String, Object[]) 16 | * @see Binding Arrays 17 | */ 18 | @Documented 19 | @Retention(RUNTIME) 20 | @Target({PARAMETER, TYPE_USE}) 21 | public @interface TypeName { 22 | 23 | /** 24 | * Defines the name of a type. 25 | * 26 | * @return the name of a type 27 | */ 28 | String value(); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/spi/SnakeCase.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.spi; 2 | 3 | final class SnakeCase implements NamingStrategy { 4 | 5 | static final NamingStrategy INSTANCE = new SnakeCase(); 6 | 7 | private SnakeCase() { 8 | super(); 9 | } 10 | 11 | @Override 12 | public String translateToDatabase(String javaName) { 13 | StringBuilder builder = new StringBuilder(); 14 | boolean wasUpperCase = false; 15 | for (int i = 0; i < javaName.length(); i++) { 16 | char c = javaName.charAt(i); 17 | if (i != 0) { 18 | boolean isUpperCase = isUpperCase(c); 19 | if (isUpperCase && !wasUpperCase) { 20 | builder.append('_'); 21 | } 22 | wasUpperCase = isUpperCase; 23 | } 24 | builder.append(c); 25 | } 26 | return builder.toString(); 27 | } 28 | 29 | private static boolean isUpperCase(char c) { 30 | return c >= 'A' && c <= 'Z'; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/SQLExceptionAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.sql.SQLException; 4 | 5 | /** 6 | * Translates a checked {@link SQLException} to an unchecked exception. 7 | * 8 | *

Very similar to {@link org.springframework.jdbc.support.SQLExceptionTranslator}.

9 | */ 10 | @FunctionalInterface 11 | public interface SQLExceptionAdapter { 12 | 13 | /** 14 | * Translates a checked {@link SQLException} to an unchecked exception. 15 | * Does not throw the exception, only creates an instance 16 | * 17 | * @param procedureName the SQL procedure name derived by this library 18 | * @param sql the JDBC call string generated by this library 19 | * @param exception the exception to translate, should be passed as cause to 20 | * the new exception instance returned by this method 21 | * @return the unchecked exception instance 22 | */ 23 | RuntimeException translate(String procedureName, String sql, SQLException exception); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/UncheckedSQLException.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.sql.SQLException; 4 | import java.util.Objects; 5 | 6 | /** 7 | * Wraps an {@link SQLException} with an unchecked exception. 8 | * 9 | * @see java.io.UncheckedIOException 10 | */ 11 | public final class UncheckedSQLException extends RuntimeException { 12 | 13 | private static final long serialVersionUID = 1L; 14 | 15 | UncheckedSQLException(SQLException cause) { 16 | super(Objects.requireNonNull(cause)); 17 | } 18 | 19 | UncheckedSQLException(String message, SQLException cause) { 20 | super(message, Objects.requireNonNull(cause)); 21 | } 22 | 23 | /** 24 | * Convenience method that returns the cause as type {@link SQLException} 25 | * avoiding the need to cast the result. 26 | * 27 | * @return the exception cause, never {@code null} 28 | */ 29 | @Override 30 | public SQLException getCause() { 31 | return (SQLException) super.getCause(); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/annotations/ParameterType.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.annotations; 2 | 3 | import static java.lang.annotation.ElementType.PARAMETER; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | import com.github.marschall.storedprocedureproxy.spi.TypeMapper; 11 | 12 | /** 13 | * Defines the SQL type of an in parameter. 14 | * 15 | * @see Binding Parameters 16 | */ 17 | @Documented 18 | @Retention(RUNTIME) 19 | @Target(PARAMETER) 20 | public @interface ParameterType { 21 | 22 | /** 23 | * Defines the SQL type of an in parameter. If nothing is specified the default 24 | * from {@link TypeMapper} is used. 25 | * 26 | * @return the parameter SQL type, can be a vendor type 27 | * @see java.sql.Types 28 | */ 29 | int value(); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ToStringUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.lang.annotation.Annotation; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ProcedureCaller; 10 | 11 | public class ToStringUtilsTest { 12 | 13 | @Test 14 | public void fetchSizeToString() { 15 | assertEquals("default", ToStringUtils.fetchSizeToString(ProcedureCaller.DEFAULT_FETCH_SIZE)); 16 | assertEquals("1", ToStringUtils.fetchSizeToString(1)); 17 | } 18 | 19 | @Test 20 | public void classNameToString() { 21 | assertEquals("String", ToStringUtils.classNameToString(java.lang.String.class)); 22 | assertEquals("java.lang.annotation.Annotation", ToStringUtils.classNameToString(Annotation.class)); 23 | assertEquals("com.github.marschall.storedprocedureproxy.ToStringUtilsTest", ToStringUtils.classNameToString(ToStringUtilsTest.class)); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/annotations/Schema.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.annotations; 2 | 3 | import static java.lang.annotation.ElementType.TYPE; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * Defines the name of a database schema. 12 | * 13 | *

If the schema name is not static you can use something like this:

14 | *
ProcedureCallerFactory.of(MyProcedures.class, dataSource)
15 |  *  .withSchemaNamingStrategy(ignored -> computeSchemaName())
16 |  *  .build();
17 | * 18 | *

For PL/SQL packages or DB2 modules {@link Namespace} should be 19 | * used.

20 | */ 21 | @Documented 22 | @Retention(RUNTIME) 23 | @Target(TYPE) 24 | public @interface Schema { 25 | 26 | 27 | /** 28 | * Defines the name of the database schema. 29 | * 30 | * @return the name of the database schema 31 | */ 32 | String value() default ""; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/annotations/ParameterName.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.annotations; 2 | 3 | import static java.lang.annotation.ElementType.PARAMETER; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 11 | 12 | /** 13 | * Defines the name of an in parameter. Only used if the parameter registration 14 | * is either {@link ParameterRegistration#NAME_ONLY} or 15 | * {@link ParameterRegistration#NAME_AND_TYPE}. 16 | * 17 | * @see Binding Parameters 18 | */ 19 | @Documented 20 | @Retention(RUNTIME) 21 | @Target(PARAMETER) 22 | public @interface ParameterName { 23 | 24 | /** 25 | * Defines the name of the in parameter. 26 | * 27 | * @return the name of the in parameter 28 | */ 29 | String value(); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/OracleTypeMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import com.github.marschall.storedprocedureproxy.spi.TypeMapper; 4 | 5 | /** 6 | * Like {@link DefaultTypeMapper} but maps {@code boolean} to 7 | * 252. 8 | * 9 | * @see OracleTypes.PLSQL_BOOLEAN 10 | */ 11 | final class OracleTypeMapper implements TypeMapper { 12 | 13 | static final TypeMapper INSTANCE = new OracleTypeMapper(); 14 | 15 | private static final int PLSQL_BOOLEAN = 252; 16 | 17 | private OracleTypeMapper() { 18 | // private constructor, avoid instantiation 19 | super(); 20 | } 21 | 22 | @Override 23 | public int mapToSqlType(Class javaType) { 24 | if (javaType == Boolean.class || javaType == boolean.class) { 25 | return PLSQL_BOOLEAN; 26 | } 27 | return DefaultTypeMapper.INSTANCE.mapToSqlType(javaType); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/ValueExtractor.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.sql.ResultSet; 4 | import java.sql.SQLException; 5 | 6 | /** 7 | * Extracts a value from a single row. 8 | * 9 | *

This class is used to extract a value object from every row in a 10 | * ref cursor out parameter.

11 | * 12 | *

Implementations should not catch {@link SQLException} this will 13 | * be done by a higher layer.

14 | * 15 | * @param the value type 16 | * @see NumberedValueExtractor 17 | */ 18 | @FunctionalInterface 19 | public interface ValueExtractor { 20 | 21 | /** 22 | * Extract the value from the current row. 23 | * 24 | *

Implementations should not call {@link ResultSet#next()} but 25 | * instead expect to be called for every method. 26 | * 27 | * @param resultSet the ResultSet to the value of the current row from 28 | * @return the value for the current row 29 | * @throws SQLException propagated if a method on {@link ResultSet} throws an exception 30 | */ 31 | V extractValue(ResultSet resultSet) throws SQLException; 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/annotations/Namespace.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.annotations; 2 | 3 | import static java.lang.annotation.ElementType.TYPE; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * Defines the namespace of a stored procedure. 12 | * 13 | *

This should be used for Oracle 14 | * PL/SQL Packages 15 | * or IBM DB2 Modules.

16 | * 17 | * @see Oracle Packages 18 | */ 19 | @Documented 20 | @Retention(RUNTIME) 21 | @Target(TYPE) 22 | public @interface Namespace { 23 | 24 | 25 | /** 26 | * Defines the namespace of a stored procedure. 27 | * 28 | * @return the namespace of a stored procedure 29 | */ 30 | String value() default ""; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/configuration/MariaDBConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.configuration; 2 | 3 | import javax.sql.DataSource; 4 | 5 | import org.springframework.beans.factory.BeanCreationException; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.jdbc.datasource.SingleConnectionDataSource; 9 | 10 | @Configuration 11 | public class MariaDBConfiguration { 12 | 13 | @Bean 14 | public DataSource dataSource() { 15 | try { 16 | Class.forName("org.mariadb.jdbc.Driver"); 17 | } catch (ClassNotFoundException e) { 18 | throw new BeanCreationException("mariadb driver not present", e); 19 | } 20 | SingleConnectionDataSource dataSource = new SingleConnectionDataSource(); 21 | dataSource.setSuppressClose(true); 22 | // https://mariadb.com/kb/en/mariadb/about-mariadb-connector-j/ 23 | dataSource.setUrl("jdbc:mariadb://localhost:3307/jdbc"); 24 | dataSource.setUsername("jdbc"); 25 | dataSource.setPassword("Cent-Quick-Space-Bath-8"); 26 | return dataSource; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/resources/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Philippe Marschall (philippe.marschall@gmail.com) 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a 5 | copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 16 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 19 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 21 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/configuration/OracleConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.configuration; 2 | 3 | import java.util.Properties; 4 | 5 | import javax.sql.DataSource; 6 | 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.jdbc.datasource.SingleConnectionDataSource; 10 | 11 | import oracle.jdbc.OracleConnection; 12 | 13 | @Configuration 14 | public class OracleConfiguration { 15 | 16 | @Bean 17 | public DataSource dataSource() { 18 | oracle.jdbc.OracleDriver.isDebug(); 19 | SingleConnectionDataSource dataSource = new SingleConnectionDataSource(); 20 | dataSource.setSuppressClose(true); 21 | dataSource.setUrl("jdbc:oracle:thin:@localhost:1521/FREEPDB1"); 22 | dataSource.setUsername("jdbc"); 23 | dataSource.setPassword("Cent-Quick-Space-Bath-8"); 24 | Properties connectionProperties = new Properties(); 25 | connectionProperties.setProperty(OracleConnection.CONNECTION_PROPERTY_THIN_NET_DISABLE_OUT_OF_BAND_BREAK, "true"); 26 | dataSource.setConnectionProperties(connectionProperties); 27 | return dataSource; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/configuration/MssqlConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.configuration; 2 | 3 | import java.sql.SQLException; 4 | 5 | import javax.sql.DataSource; 6 | 7 | import org.springframework.beans.factory.BeanCreationException; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.jdbc.datasource.SingleConnectionDataSource; 11 | 12 | @Configuration 13 | public class MssqlConfiguration { 14 | 15 | @Bean 16 | public DataSource dataSource() { 17 | try { 18 | if (!com.microsoft.sqlserver.jdbc.SQLServerDriver.isRegistered()) { 19 | com.microsoft.sqlserver.jdbc.SQLServerDriver.register(); 20 | } 21 | } catch (SQLException e) { 22 | throw new BeanCreationException("could not register driver", e); 23 | } 24 | SingleConnectionDataSource dataSource = new SingleConnectionDataSource(); 25 | dataSource.setSuppressClose(true); 26 | dataSource.setUrl("jdbc:sqlserver://localhost:1433;databaseName=master;encrypt=false"); 27 | dataSource.setUsername("sa"); 28 | dataSource.setPassword("Cent-Quick-Space-Bath-8"); 29 | return dataSource; 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/SpringSQLExceptionAdapterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertNotNull; 4 | import static org.mockito.ArgumentMatchers.any; 5 | import static org.mockito.ArgumentMatchers.anyString; 6 | import static org.mockito.Mockito.mock; 7 | import static org.mockito.Mockito.when; 8 | 9 | import java.sql.SQLException; 10 | 11 | import org.junit.jupiter.api.Test; 12 | import org.springframework.dao.DataAccessException; 13 | import org.springframework.jdbc.support.SQLExceptionTranslator; 14 | 15 | class SpringSQLExceptionAdapterTest { 16 | 17 | /** 18 | * Regression test for #71 19 | */ 20 | @Test 21 | void translateReturnsNull() { 22 | SQLExceptionTranslator translator = mock(SQLExceptionTranslator.class); 23 | when(translator.translate(anyString(), anyString(), any(SQLException.class))).thenReturn(null); 24 | 25 | SpringSQLExceptionAdapter adapter = new SpringSQLExceptionAdapter(translator); 26 | 27 | DataAccessException translated = adapter.translate("simple_function", "{call simple_function()}", new SQLException()); 28 | assertNotNull(translated); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java16/com/github/marschall/storedprocedureproxy/Java16DefaultMethodSupport.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.reflect.InvocationHandler; 6 | import java.lang.reflect.Method; 7 | 8 | final class Java16DefaultMethodSupport implements DefaultMethodSupport { 9 | 10 | static final DefaultMethodSupport INSTANCE = new Java16DefaultMethodSupport(); 11 | 12 | private static final MethodHandle INVOKE_DEFAULT; 13 | 14 | static { 15 | MethodHandle invokeDefaultHandle; 16 | try { 17 | Method invokeDefaultMethod = InvocationHandler.class 18 | .getDeclaredMethod("invokeDefault", Object.class, Method.class, Object[].class); 19 | invokeDefaultHandle = MethodHandles.lookup().unreflect(invokeDefaultMethod); 20 | } catch (ReflectiveOperationException e) { 21 | throw new RuntimeException("could not initialize class", e); 22 | } 23 | INVOKE_DEFAULT = invokeDefaultHandle; 24 | } 25 | 26 | private Java16DefaultMethodSupport() { 27 | super(); 28 | } 29 | 30 | @Override 31 | public Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable { 32 | return INVOKE_DEFAULT.invokeExact(proxy, method, args); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/configuration/FirebirdConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.configuration; 2 | 3 | import javax.sql.DataSource; 4 | 5 | import org.springframework.beans.factory.BeanCreationException; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.jdbc.datasource.SingleConnectionDataSource; 9 | 10 | @Configuration 11 | public class FirebirdConfiguration { 12 | 13 | 14 | @Bean 15 | public DataSource dataSource() { 16 | try { 17 | Class.forName("org.firebirdsql.jdbc.FBDriver"); 18 | } catch (ClassNotFoundException e) { 19 | throw new BeanCreationException("firebird driver not present", e); 20 | } 21 | SingleConnectionDataSource dataSource = new SingleConnectionDataSource(); 22 | dataSource.setSuppressClose(true); 23 | // https://www.firebirdsql.org/file/documentation/drivers_documentation/java/faq.html#jdbc-urls-java.sql.drivermanager 24 | // https://github.com/FirebirdSQL/jaybird/wiki/Jaybird-and-Firebird-3 25 | dataSource.setUrl("jdbc:firebirdsql://localhost:3050/jdbc?charSet=utf-8"); 26 | // https://github.com/almeida/docker-firebird 27 | dataSource.setUsername("jdbc"); 28 | dataSource.setPassword("Cent-Quick-Space-Bath-8"); 29 | return dataSource; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/configuration/PostgresConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.configuration; 2 | 3 | import static com.github.marschall.storedprocedureproxy.Travis.isTravis; 4 | 5 | import java.sql.SQLException; 6 | 7 | import javax.sql.DataSource; 8 | 9 | import org.springframework.beans.factory.BeanCreationException; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.jdbc.datasource.SingleConnectionDataSource; 13 | 14 | @Configuration 15 | public class PostgresConfiguration { 16 | 17 | @Bean 18 | public DataSource dataSource() { 19 | try { 20 | if (!org.postgresql.Driver.isRegistered()) { 21 | org.postgresql.Driver.register(); 22 | } 23 | } catch (SQLException e) { 24 | throw new BeanCreationException("could not register driver", e); 25 | } 26 | SingleConnectionDataSource dataSource = new SingleConnectionDataSource(); 27 | dataSource.setSuppressClose(true); 28 | String userName = System.getProperty("user.name"); 29 | // defaults from Postgres.app 30 | dataSource.setUrl("jdbc:postgresql:" + userName); 31 | dataSource.setUsername(userName); 32 | String password = isTravis() ? "" : "Cent-Quick-Space-Bath-8"; 33 | dataSource.setPassword(password); 34 | return dataSource; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/Usage.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import javax.naming.InitialContext; 4 | import javax.naming.NamingException; 5 | import javax.sql.DataSource; 6 | 7 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 8 | import com.github.marschall.storedprocedureproxy.procedures.PostgresProcedures; 9 | 10 | public class Usage { 11 | 12 | public static void simpleUsage() throws NamingException { 13 | DataSource dataSource = (DataSource) new InitialContext().lookup("java:comp/DefaultDataSource"); 14 | Class inferfaceDeclaration = PostgresProcedures.class; 15 | PostgresProcedures procedures = ProcedureCallerFactory.build(inferfaceDeclaration, dataSource); 16 | procedures.browserVersion("safari", "9.0"); 17 | } 18 | 19 | public static void advancedUsage() throws NamingException { 20 | DataSource dataSource = (DataSource) new InitialContext().lookup("java:comp/DefaultDataSource"); 21 | Class inferfaceDeclaration = PostgresProcedures.class; 22 | PostgresProcedures procedures = ProcedureCallerFactory.of(inferfaceDeclaration, dataSource) 23 | .withParameterRegistration(ParameterRegistration.INDEX_AND_TYPE) 24 | .withPostgresArrays() 25 | .build(); 26 | procedures.browserVersion("safari", "9.0"); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/procedures/OraclePackageProcedures.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.procedures; 2 | 3 | import java.util.List; 4 | 5 | import com.github.marschall.storedprocedureproxy.annotations.InOutParameter; 6 | import com.github.marschall.storedprocedureproxy.annotations.Namespace; 7 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 8 | import com.github.marschall.storedprocedureproxy.annotations.ProcedureName; 9 | import com.github.marschall.storedprocedureproxy.annotations.ReturnValue; 10 | import com.github.marschall.storedprocedureproxy.annotations.TypeName; 11 | 12 | import oracle.jdbc.OracleTypes; 13 | 14 | @Namespace("stored_procedure_proxy") 15 | public interface OraclePackageProcedures { 16 | 17 | @ReturnValue 18 | @ProcedureName("negate_function") 19 | boolean negateFunction(boolean b); 20 | 21 | @InOutParameter 22 | @ProcedureName("negate_procedure") 23 | boolean negateProcedure(boolean b); 24 | 25 | @OutParameter(name = "sum_result") 26 | @ProcedureName("array_sum") 27 | int sum(@TypeName("STORED_PROCEDURE_PROXY_ARRAY") int[] ids); 28 | 29 | @OutParameter(name = "ARRAY_RESULT", typeName = "STORED_PROCEDURE_PROXY_ARRAY") 30 | @ProcedureName("return_array") 31 | int[] arrayResult(); 32 | 33 | @OutParameter(name = "ids", type = OracleTypes.CURSOR) 34 | @ProcedureName("return_refcursor") 35 | List returnRefcursor(); 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/H2ProcedureSources.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.sql.ResultSet; 4 | import java.sql.SQLException; 5 | import java.sql.Types; 6 | import java.util.logging.Logger; 7 | 8 | import org.h2.tools.SimpleResultSet; 9 | 10 | public class H2ProcedureSources { 11 | 12 | private static final Logger LOG = Logger.getLogger("H2ProcedureSources"); 13 | 14 | public static String stringProcedure(String input) { 15 | return "pre" + input + "post"; 16 | } 17 | 18 | public static void voidProcedure(String input) { 19 | LOG.fine(input); 20 | } 21 | 22 | public static String noArgProcedure() { 23 | return "output"; 24 | } 25 | 26 | public static Integer[] reverseIntegerArray(Integer[] src) { 27 | Integer[] target = new Integer[src.length]; 28 | for (int i = 0; i < src.length; i++) { 29 | target[target.length - i - 1] = src[i]; 30 | } 31 | return target; 32 | } 33 | 34 | public static Integer[] returnIntegerArray() { 35 | return new Integer[] {4, 1, 7}; 36 | } 37 | 38 | public static ResultSet simpleResultSet() throws SQLException { 39 | SimpleResultSet resultSet = new SimpleResultSet(); 40 | resultSet.addColumn("ID", Types.INTEGER, 10, 0); 41 | resultSet.addColumn("NAME", Types.VARCHAR, 255, 0); 42 | resultSet.addRow(0L, "Hello"); 43 | resultSet.addRow(1L, "World"); 44 | return resultSet; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/DisabledOnTravisCondition.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; 4 | 5 | import java.lang.reflect.AnnotatedElement; 6 | import java.util.Optional; 7 | 8 | import org.junit.jupiter.api.extension.ConditionEvaluationResult; 9 | import org.junit.jupiter.api.extension.ExecutionCondition; 10 | import org.junit.jupiter.api.extension.ExtensionContext; 11 | 12 | public final class DisabledOnTravisCondition implements ExecutionCondition { 13 | 14 | private static final ConditionEvaluationResult NOT_ANNOTATED = 15 | ConditionEvaluationResult.enabled("@DisabledOnTravis is not present"); 16 | 17 | private static final ConditionEvaluationResult NOT_TRAVIS_CI = 18 | ConditionEvaluationResult.enabled("Not on TravisCI"); 19 | 20 | @Override 21 | public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { 22 | Optional element = context.getElement(); 23 | Optional disabled = findAnnotation(element, DisabledOnTravis.class); 24 | if (disabled.isPresent()) { 25 | if (Travis.isTravis()) { 26 | String reason = element.get() + " is disabled on TravisCI"; 27 | return ConditionEvaluationResult.disabled(reason); 28 | } else { 29 | return NOT_TRAVIS_CI; 30 | } 31 | } 32 | 33 | return NOT_ANNOTATED; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/annotations/FetchSize.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.annotations; 2 | 3 | import static java.lang.annotation.ElementType.METHOD; 4 | import static java.lang.annotation.ElementType.TYPE; 5 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 6 | 7 | import java.lang.annotation.Documented; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.Target; 10 | import java.sql.ResultSet; 11 | 12 | /** 13 | * Allows manual control over the fetch size. 14 | * 15 | *

This is only really useful for methods that use cursors or 16 | * {@link ResultSet}s to return multiple rows.

17 | * 18 | *

When applied to an interface applies to all methods in the class 19 | * unless also applied to a method.

20 | * 21 | * @see java.sql.Statement#setFetchSize(int) 22 | * @see Fetch Size 23 | */ 24 | @Documented 25 | @Retention(RUNTIME) 26 | @Target({METHOD, TYPE}) 27 | public @interface FetchSize { 28 | 29 | /** 30 | * Sets the fetch size to be used. 31 | * 32 | *

The usual disclaimers for setting the fetch size apply:

33 | *
    34 | *
  • it's just a hint
  • 35 | *
  • the driver may ignore it
  • 36 | *
  • the driver may round it to a value more convenient
  • 37 | *
38 | * 39 | * @return the number of rows to fetch in one round trip 40 | */ 41 | int value(); 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/configuration/MysqlConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.configuration; 2 | 3 | import static com.github.marschall.storedprocedureproxy.Travis.isTravis; 4 | 5 | import javax.sql.DataSource; 6 | 7 | import org.springframework.beans.factory.BeanCreationException; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.jdbc.datasource.SingleConnectionDataSource; 11 | 12 | @Configuration 13 | public class MysqlConfiguration { 14 | 15 | @Bean 16 | public DataSource dataSource() { 17 | try { 18 | Class.forName("com.mysql.cj.jdbc.Driver"); 19 | } catch (ClassNotFoundException e) { 20 | throw new BeanCreationException("mysql driver not present", e); 21 | } 22 | SingleConnectionDataSource dataSource = new SingleConnectionDataSource(); 23 | dataSource.setSuppressClose(true); 24 | String userName = System.getProperty("user.name"); 25 | String database = userName; 26 | // https://dev.mysql.com/doc/connector-j/6.0/en/connector-j-reference-configuration-properties.html 27 | dataSource.setUrl("jdbc:mysql://localhost:3306/" + database + "?useSSL=false&allowPublicKeyRetrieval=true&logger=com.mysql.cj.log.Slf4JLogger&disableMariaDbDriver=true"); 28 | dataSource.setUsername(userName); 29 | String password = isTravis() ? "" : userName; 30 | dataSource.setPassword(password); 31 | return dataSource; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/MethodHandleTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.lang.invoke.MethodHandle; 6 | import java.lang.invoke.MethodHandles; 7 | import java.lang.reflect.Method; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class MethodHandleTest { 12 | 13 | private static final MethodHandle CREATE_ARRAY; 14 | 15 | static { 16 | try { 17 | Method method = OracleInterface.class.getDeclaredMethod("createArray", String.class, Object.class); 18 | CREATE_ARRAY = MethodHandles.publicLookup().unreflect(method); 19 | } catch (ReflectiveOperationException e) { 20 | throw new ExceptionInInitializerError(e); 21 | } 22 | } 23 | 24 | @Test 25 | public void createArray() throws Throwable { 26 | Object oracle = new OracleImplementation(); 27 | 28 | String s = (String) CREATE_ARRAY.invoke(oracle, "string", (Object) 1); 29 | assertEquals("s: string, o: 1", s); 30 | } 31 | 32 | public interface OracleInterface { 33 | 34 | String createArray(String s, Object o); 35 | 36 | int[] getIntArray(); 37 | 38 | } 39 | 40 | static class OracleImplementation implements OracleInterface { 41 | 42 | @Override 43 | public String createArray(String s, Object o) { 44 | return "s: " + s + ", o: " + o; 45 | } 46 | 47 | @Override 48 | public int[] getIntArray() { 49 | return new int[] {1, 2, 3}; 50 | } 51 | 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/SpringSQLExceptionAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.sql.SQLException; 4 | 5 | import javax.sql.DataSource; 6 | 7 | import org.springframework.dao.DataAccessException; 8 | import org.springframework.jdbc.UncategorizedSQLException; 9 | import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; 10 | import org.springframework.jdbc.support.SQLExceptionTranslator; 11 | 12 | /** 13 | * A {@link SQLExceptionAdapter} that delegates to a {@link SQLExceptionTranslator}. 14 | */ 15 | final class SpringSQLExceptionAdapter implements SQLExceptionAdapter { 16 | 17 | private final SQLExceptionTranslator translator; 18 | 19 | SpringSQLExceptionAdapter(SQLExceptionTranslator translator) { 20 | this.translator = translator; 21 | } 22 | 23 | SpringSQLExceptionAdapter(DataSource dataSource) { 24 | // the same code that org.springframework.jdbc.support.JdbcAccessor#getExceptionTranslator() uses 25 | this(new SQLErrorCodeSQLExceptionTranslator(dataSource)); 26 | } 27 | 28 | @Override 29 | public DataAccessException translate(String procedureName, String sql, SQLException ex) { 30 | DataAccessException translated = this.translator.translate("calling procedure " + procedureName, sql, ex); 31 | if (translated != null) { 32 | return translated; 33 | } else { 34 | // #translate may return null as per contract 35 | return new UncategorizedSQLException("failed to call procedure: " + procedureName, sql, ex); 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/DefaultTypeNameResolverTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.lang.reflect.Method; 6 | import java.lang.reflect.Parameter; 7 | import java.util.Collection; 8 | import java.util.List; 9 | 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | 13 | import com.github.marschall.storedprocedureproxy.spi.TypeNameResolver; 14 | 15 | public class DefaultTypeNameResolverTest { 16 | 17 | private TypeNameResolver typeNameResolver; 18 | 19 | @BeforeEach 20 | public void setUp() { 21 | this.typeNameResolver = DefaultTypeNameResolver.INSTANCE; 22 | } 23 | 24 | @Test 25 | public void getTypeName() throws ReflectiveOperationException { 26 | Method method = SampleInterface.class.getDeclaredMethod("sampleMethod", List.class, Collection.class, Integer[].class, int[].class); 27 | Parameter[] parameters = method.getParameters(); 28 | 29 | assertEquals("VARCHAR", this.typeNameResolver.resolveTypeName(parameters[0])); 30 | assertEquals("BIGINT", this.typeNameResolver.resolveTypeName(parameters[1])); 31 | assertEquals("INTEGER", this.typeNameResolver.resolveTypeName(parameters[2])); 32 | assertEquals("INTEGER", this.typeNameResolver.resolveTypeName(parameters[3])); 33 | } 34 | 35 | interface SampleInterface { 36 | 37 | 38 | void sampleMethod(List stringList, Collection longCollection, Integer[] referenceArray, int[] primitveArray); 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/NumberedValueExtractor.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.sql.ResultSet; 4 | import java.sql.SQLException; 5 | 6 | /** 7 | * Extracts a value from a single row. 8 | * 9 | *

This class is used to extract a value object from every row in a 10 | * ref cursor out parameter.

11 | * 12 | *

This class is modeled after Springs 13 | * {@link org.springframework.jdbc.core.RowMapper}. If you're using 14 | * lambdas the the code should directly port over. If not the easiest 15 | * way to bridge the code is using an method reference.

16 | * 17 | *

Implementations should not catch {@link SQLException} this will 18 | * be done by a higher layer.

19 | * 20 | * @param the value type 21 | * @see ValueExtractor 22 | * @see org.springframework.jdbc.core.RowMapper 23 | */ 24 | @FunctionalInterface 25 | public interface NumberedValueExtractor { 26 | 27 | /** 28 | * Extract the value from the current row. 29 | * 30 | *

Implementations should not call {@link ResultSet#next()} but 31 | * instead expect to be called for every method. 32 | * 33 | * @param resultSet the ResultSet to the value of the current row from 34 | * @param rowNumber 35 | * the 0-based index of the current row, mostly for Spring compatibility 36 | * @return the value for the current row 37 | * @throws SQLException propagated if a method on {@link ResultSet} throws an exception 38 | * @see org.springframework.jdbc.core.RowMapper#mapRow(ResultSet, int) 39 | */ 40 | V extractValue(ResultSet resultSet, int rowNumber) throws SQLException; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/ToStringUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ProcedureCaller; 4 | 5 | final class ToStringUtils { 6 | 7 | private ToStringUtils() { 8 | throw new AssertionError("not instantiable"); 9 | } 10 | 11 | static String fetchSizeToString(int fetchSize) { 12 | if (fetchSize == ProcedureCaller.DEFAULT_FETCH_SIZE) { 13 | return "default"; 14 | } else { 15 | return Integer.toString(fetchSize); 16 | } 17 | } 18 | 19 | static String classNameToString(Class clazz) { 20 | if (clazz.getPackage().getName().equals("java.lang")) { 21 | return clazz.getSimpleName(); 22 | } else { 23 | return clazz.getName(); 24 | } 25 | } 26 | 27 | static void toStringOn(String[] array, StringBuilder builder) { 28 | for (int i = 0; i < array.length; i++) { 29 | if (i > 0) { 30 | builder.append(", "); 31 | } 32 | String element = array[i]; 33 | builder.append(element); 34 | } 35 | } 36 | 37 | static void toStringOn(Object[] array, StringBuilder builder) { 38 | for (int i = 0; i < array.length; i++) { 39 | if (i > 0) { 40 | builder.append(", "); 41 | } 42 | Object element = array[i]; 43 | builder.append(element); 44 | } 45 | } 46 | 47 | static void toStringOn(int[] array, StringBuilder builder) { 48 | for (int i = 0; i < array.length; i++) { 49 | if (i > 0) { 50 | builder.append(", "); 51 | } 52 | int element = array[i]; 53 | builder.append(element); 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/procedures/H2Procedures.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.procedures; 2 | 3 | import java.sql.ResultSet; 4 | import java.util.List; 5 | import java.util.function.Function; 6 | 7 | import com.github.marschall.storedprocedureproxy.NumberedValueExtractor; 8 | import com.github.marschall.storedprocedureproxy.ValueExtractor; 9 | import com.github.marschall.storedprocedureproxy.annotations.ReturnValue; 10 | 11 | public interface H2Procedures { 12 | 13 | @ReturnValue 14 | String stringProcedure(String input); 15 | 16 | void voidProcedure(String input); 17 | 18 | @ReturnValue 19 | String noArgProcedure(); 20 | 21 | @ReturnValue 22 | Integer[] reverseIntegerArray(Integer[] input); 23 | 24 | @ReturnValue 25 | Integer[] returnIntegerArray(); 26 | 27 | List simpleResultSet(NumberedValueExtractor extractor); 28 | 29 | List simpleResultSet(ValueExtractor extractor); 30 | 31 | List simpleResultSet(Function extractor); 32 | 33 | default List simpleResultSet() { 34 | return this.simpleResultSet((rs, i) -> { 35 | long id = rs.getLong("ID"); 36 | String name = rs.getString("NAME"); 37 | return new IdName(id, name); 38 | }); 39 | } 40 | 41 | public static final class IdName { 42 | 43 | private final long id; 44 | private final String name; 45 | 46 | public IdName(long id, String name) { 47 | this.id = id; 48 | this.name = name; 49 | } 50 | 51 | public long getId() { 52 | return this.id; 53 | } 54 | 55 | public String getName() { 56 | return this.name; 57 | } 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/MssqlTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.util.Arrays; 6 | 7 | import org.springframework.test.context.ContextConfiguration; 8 | import org.springframework.test.context.jdbc.Sql; 9 | 10 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 11 | import com.github.marschall.storedprocedureproxy.configuration.MssqlConfiguration; 12 | import com.github.marschall.storedprocedureproxy.procedures.MssqlProcedures; 13 | 14 | @Sql("classpath:sql/mssql_procedures.sql") 15 | @ContextConfiguration(classes = MssqlConfiguration.class) 16 | @DisabledOnTravis 17 | public class MssqlTest extends AbstractDataSourceTest { 18 | 19 | private MssqlProcedures procedures(ParameterRegistration parameterRegistration) { 20 | return ProcedureCallerFactory.of(MssqlProcedures.class, this.getDataSource()) 21 | .withParameterRegistration(parameterRegistration) 22 | .build(); 23 | } 24 | 25 | @IndexedParametersRegistrationTest 26 | public void plus1inout(ParameterRegistration parameterRegistration) { 27 | assertEquals(2, this.procedures(parameterRegistration).plus1inout(1)); 28 | } 29 | 30 | @IndexedParametersRegistrationTest 31 | public void plus1inret(ParameterRegistration parameterRegistration) { 32 | assertEquals(2, this.procedures(parameterRegistration).plus1inret(1)); 33 | } 34 | 35 | @IndexedParametersRegistrationTest 36 | public void fakeCursor(ParameterRegistration parameterRegistration) { 37 | assertEquals(Arrays.asList("hello", "world"), this.procedures(parameterRegistration).fakeCursor()); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/AnnotationBasedTypeNameResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.lang.reflect.AnnotatedParameterizedType; 4 | import java.lang.reflect.AnnotatedType; 5 | import java.lang.reflect.Parameter; 6 | 7 | import com.github.marschall.storedprocedureproxy.annotations.TypeName; 8 | import com.github.marschall.storedprocedureproxy.spi.TypeNameResolver; 9 | 10 | final class AnnotationBasedTypeNameResolver implements TypeNameResolver { 11 | 12 | static final TypeNameResolver INSTANCE = new AnnotationBasedTypeNameResolver(); 13 | 14 | private AnnotationBasedTypeNameResolver() { 15 | super(); 16 | } 17 | 18 | @Override 19 | public String resolveTypeName(Parameter parameter) { 20 | // REVIEW maybe move to dedicated reflection class 21 | if (parameter.isAnnotationPresent(TypeName.class)) { 22 | return parameter.getAnnotation(TypeName.class).value(); 23 | } else { 24 | AnnotatedType annotatedType = parameter.getAnnotatedType(); 25 | if (annotatedType instanceof AnnotatedParameterizedType) { 26 | AnnotatedParameterizedType annotatedParameterizedType = (AnnotatedParameterizedType) annotatedType; 27 | AnnotatedType[] annotatedActualTypeArguments = annotatedParameterizedType.getAnnotatedActualTypeArguments(); 28 | if (annotatedActualTypeArguments != null && annotatedActualTypeArguments.length > 0) { 29 | AnnotatedType stringParameterType = annotatedActualTypeArguments[0]; 30 | if (stringParameterType.isAnnotationPresent(TypeName.class)) { 31 | return stringParameterType.getAnnotation(TypeName.class).value(); 32 | } 33 | } 34 | } 35 | return null; 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/Db2Test.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Disabled; 6 | import org.springframework.test.context.ContextConfiguration; 7 | import org.springframework.test.context.jdbc.Sql; 8 | import org.springframework.test.context.jdbc.SqlConfig; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 12 | import com.github.marschall.storedprocedureproxy.configuration.Db2Configuration; 13 | import com.github.marschall.storedprocedureproxy.procedures.Db2Procedures; 14 | 15 | @Disabled 16 | @Transactional 17 | @ContextConfiguration(classes = Db2Configuration.class) 18 | @Sql(scripts = "classpath:sql/db2_procedures.sql", config = @SqlConfig(separator = "/")) 19 | public class Db2Test extends AbstractDataSourceTest { 20 | 21 | private Db2Procedures procedures(ParameterRegistration parameterRegistration) { 22 | return ProcedureCallerFactory.of(Db2Procedures.class, this.getDataSource()) 23 | .withParameterRegistration(parameterRegistration) 24 | .build(); 25 | } 26 | 27 | @Disabled("function calling seems to be broken on DB2") 28 | @IndexedParametersRegistrationTest 29 | public void salesTax(ParameterRegistration parameterRegistration) { 30 | assertEquals(0.01f, 6.0f, this.procedures(parameterRegistration).salesTax(100.0f)); 31 | } 32 | 33 | @AllParametersRegistrationTest 34 | public void propertyTax(ParameterRegistration parameterRegistration) { 35 | assertEquals(0.01f, 6.0f, this.procedures(parameterRegistration).propertyTax(100.0f)); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/annotations/InOutParameter.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.annotations; 2 | 3 | import static java.lang.annotation.ElementType.METHOD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | /** 11 | * Signals that the procedure uses an inout parameter rather than 12 | * an out parameter, a return value or result set. 13 | * 14 | *

Applied to a method means that the return value should be retrieved using an 15 | * inout parameter.

16 | * 17 | *

Unlike {@link OutParameter} or {@link ReturnValue} most additional 18 | * information in taken from the method parameter

19 | * 20 | *

Causes a call string to be generated in the form of 21 | * {@code {@code "{call function_name(?)}" instead of "{ ? = call function_name()}"}} 22 | * where one of the function arguments is an out parameter.

23 | * 24 | * @see ReturnValue 25 | * @see OutParameter 26 | * @see InOut Parameter Result Extraction 27 | */ 28 | @Documented 29 | @Retention(RUNTIME) 30 | @Target(METHOD) 31 | public @interface InOutParameter { 32 | 33 | /** 34 | * Defines the index of the inout parameter. If not specified the 35 | * inout parameter is assumed to be the last parameter. 36 | * 37 | *

If the out parameter isn't the last parameter you have to 38 | * provide the index of the out parameter.

39 | * 40 | * @return the 1 based index of the out parameter 41 | */ 42 | int index() default -1; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/resources/sql/oracle_procedures.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE FUNCTION sales_tax(subtotal real) 2 | RETURN real AS 3 | BEGIN 4 | RETURN subtotal * 0.06; 5 | END; 6 | / 7 | 8 | CREATE OR REPLACE PROCEDURE property_tax(subtotal IN real, tax OUT real) AS 9 | BEGIN 10 | tax := subtotal * 0.06; 11 | END; 12 | / 13 | 14 | BEGIN 15 | EXECUTE IMMEDIATE 'CREATE OR REPLACE TYPE stored_procedure_proxy_array IS TABLE OF NUMBER(8)'; 16 | END; 17 | / 18 | 19 | 20 | CREATE OR REPLACE PACKAGE stored_procedure_proxy AS 21 | TYPE TEST_IDS IS TABLE OF NUMBER(8); 22 | FUNCTION negate_function (b BOOLEAN) 23 | RETURN BOOLEAN; 24 | PROCEDURE negate_procedure(b IN OUT BOOLEAN); 25 | PROCEDURE array_sum(ids IN stored_procedure_proxy_array, sum_result OUT NUMBER); 26 | PROCEDURE return_array(array_result OUT stored_procedure_proxy_array); 27 | PROCEDURE return_refcursor(ids OUT SYS_REFCURSOR); 28 | END stored_procedure_proxy; 29 | / 30 | 31 | CREATE OR REPLACE PACKAGE BODY stored_procedure_proxy AS 32 | FUNCTION negate_function (b BOOLEAN) 33 | RETURN BOOLEAN AS 34 | BEGIN 35 | RETURN NOT b; 36 | END; 37 | 38 | PROCEDURE negate_procedure(b IN OUT BOOLEAN) AS 39 | BEGIN 40 | b := NOT b; 41 | END; 42 | 43 | PROCEDURE array_sum(ids IN stored_procedure_proxy_array, sum_result OUT NUMBER) AS 44 | BEGIN 45 | SELECT SUM(COLUMN_VALUE) 46 | INTO sum_result 47 | FROM TABLE(ids); 48 | END; 49 | 50 | PROCEDURE return_array(array_result OUT stored_procedure_proxy_array) AS 51 | BEGIN 52 | array_result := stored_procedure_proxy_array(1, 2, 3); 53 | END; 54 | 55 | PROCEDURE return_refcursor(ids OUT SYS_REFCURSOR) AS 56 | BEGIN 57 | OPEN ids FOR 58 | SELECT level id 59 | FROM dual 60 | CONNECT BY level <= 3; 61 | END; 62 | END stored_procedure_proxy; 63 | / 64 | 65 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/DerbyTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.comparesEqualTo; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.condition.JRE.JAVA_8; 7 | 8 | import java.math.BigDecimal; 9 | 10 | import org.junit.jupiter.api.condition.DisabledOnJre; 11 | import org.springframework.test.context.ContextConfiguration; 12 | 13 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 14 | import com.github.marschall.storedprocedureproxy.configuration.DerbyConfiguration; 15 | import com.github.marschall.storedprocedureproxy.procedures.DerbyProcedures; 16 | 17 | @DisabledOnJre(JAVA_8) 18 | @ContextConfiguration(classes = DerbyConfiguration.class) 19 | public class DerbyTest extends AbstractDataSourceTest { 20 | 21 | private DerbyProcedures functions(ParameterRegistration parameterRegistration) { 22 | return ProcedureCallerFactory.of(DerbyProcedures.class, this.getDataSource()) 23 | .withParameterRegistration(parameterRegistration) 24 | .withNamespace() 25 | .build(); 26 | } 27 | 28 | @IndexedParametersRegistrationTest 29 | public void outParameter(ParameterRegistration parameterRegistration) { 30 | assertThat(this.functions(parameterRegistration).calculateRevenueByMonth(9, 2016), comparesEqualTo(new BigDecimal(201609))); 31 | } 32 | 33 | @IndexedParametersRegistrationTest 34 | public void inOutParameter(ParameterRegistration parameterRegistration) { 35 | assertThat(this.functions(parameterRegistration).raisePrice(new BigDecimal("10.1")), comparesEqualTo(new BigDecimal("20.2"))); 36 | } 37 | 38 | @IndexedParametersRegistrationTest 39 | public void returnValue(ParameterRegistration parameterRegistration) { 40 | assertEquals(0.01d, 6.0d, this.functions(parameterRegistration).salesTax(100.0d)); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/MysqlTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | import org.springframework.test.context.ContextConfiguration; 9 | import org.springframework.test.context.jdbc.Sql; 10 | 11 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 12 | import com.github.marschall.storedprocedureproxy.configuration.MysqlConfiguration; 13 | import com.github.marschall.storedprocedureproxy.procedures.MysqlProcedures; 14 | 15 | @Sql("classpath:sql/mysql_procedures.sql") 16 | @ContextConfiguration(classes = MysqlConfiguration.class) 17 | public class MysqlTest extends AbstractDataSourceTest { 18 | 19 | private MysqlProcedures procedures(ParameterRegistration parameterRegistration) { 20 | return ProcedureCallerFactory.of(MysqlProcedures.class, this.getDataSource()) 21 | .withParameterRegistration(parameterRegistration) 22 | .build(); 23 | } 24 | 25 | @IndexedParametersRegistrationTest 26 | public void helloFunction(ParameterRegistration parameterRegistration) { 27 | // names for out parameters of functions don't work 28 | 29 | assertEquals("Hello, Monty!", this.procedures(parameterRegistration).helloFunction("Monty")); 30 | } 31 | 32 | @AllParametersRegistrationTest 33 | public void helloProcedure(ParameterRegistration parameterRegistration) { 34 | assertEquals("Hello, Monty!", this.procedures(parameterRegistration).helloProcedure("Monty")); 35 | } 36 | 37 | @AllParametersRegistrationTest 38 | public void simpleRefCursor(ParameterRegistration parameterRegistration) { 39 | // https://stackoverflow.com/questions/273929/what-is-the-equivalent-of-oracle-s-ref-cursor-in-mysql-when-using-jdbc 40 | List refCursor = this.procedures(parameterRegistration).fakeRefcursor(); 41 | assertEquals(Arrays.asList("hello", "mysql"), refCursor); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ObjectMethodsTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.containsString; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertNotEquals; 7 | import static org.mockito.ArgumentMatchers.anyString; 8 | import static org.mockito.Mockito.mock; 9 | import static org.mockito.Mockito.when; 10 | 11 | import java.sql.Connection; 12 | import java.sql.DatabaseMetaData; 13 | import java.sql.SQLException; 14 | 15 | import javax.sql.DataSource; 16 | 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.Test; 19 | 20 | public class ObjectMethodsTest { 21 | 22 | private DataSource dataSource; 23 | 24 | private Connection connection; 25 | 26 | private SimpleProcedures procedures; 27 | 28 | @BeforeEach 29 | public void setUp() throws SQLException { 30 | this.dataSource = mock(DataSource.class); 31 | this.connection = mock(Connection.class); 32 | DatabaseMetaData metaData = mock(DatabaseMetaData.class); 33 | 34 | when(this.dataSource.getConnection()).thenReturn(this.connection); 35 | when(this.connection.getMetaData()).thenReturn(metaData); 36 | when(metaData.getDatabaseProductName()).thenReturn("junit"); 37 | when(this.connection.prepareCall(anyString())).thenThrow(SQLException.class); 38 | this.procedures = ProcedureCallerFactory.build(SimpleProcedures.class, this.dataSource); 39 | } 40 | 41 | @Test 42 | public void testEquals() { 43 | assertEquals(this.procedures, this.procedures); 44 | assertNotEquals(this.procedures, null); 45 | } 46 | 47 | @Test 48 | public void testToString() { 49 | assertThat(this.procedures.toString(), containsString(SimpleProcedures.class.getName())); 50 | } 51 | 52 | @Test 53 | public void testHashCode() { 54 | this.procedures.hashCode(); 55 | } 56 | 57 | interface SimpleProcedures { 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/MariaDBTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | import org.springframework.test.context.ContextConfiguration; 9 | import org.springframework.test.context.jdbc.Sql; 10 | 11 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 12 | import com.github.marschall.storedprocedureproxy.configuration.MariaDBConfiguration; 13 | import com.github.marschall.storedprocedureproxy.procedures.MariaDBProcedures; 14 | 15 | @ContextConfiguration(classes = MariaDBConfiguration.class) 16 | @Sql("classpath:sql/mariadb_procedures.sql") 17 | @DisabledOnTravis 18 | public class MariaDBTest extends AbstractDataSourceTest { 19 | 20 | private MariaDBProcedures procedures(ParameterRegistration parameterRegistration) { 21 | return ProcedureCallerFactory.of(MariaDBProcedures.class, this.getDataSource()) 22 | .withParameterRegistration(parameterRegistration) 23 | .build(); 24 | } 25 | 26 | @IndexedParametersRegistrationTest 27 | public void helloFunction(ParameterRegistration parameterRegistration) { 28 | // names for out parameters of functions don't work 29 | 30 | assertEquals("Hello, Monty!", this.procedures(parameterRegistration).helloFunction("Monty")); 31 | } 32 | 33 | @AllParametersRegistrationTest 34 | public void helloProcedure(ParameterRegistration parameterRegistration) { 35 | assertEquals("Hello, Monty!", this.procedures(parameterRegistration).helloProcedure("Monty")); 36 | } 37 | 38 | @AllParametersRegistrationTest 39 | public void simpleRefCursor(ParameterRegistration parameterRegistration) { 40 | // https://stackoverflow.com/questions/273929/what-is-the-equivalent-of-oracle-s-ref-cursor-in-mysql-when-using-jdbc 41 | List refCursor = this.procedures(parameterRegistration).fakeRefcursor(); 42 | assertEquals(Arrays.asList("hello", "mysql"), refCursor); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/FirebirdTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import java.util.Arrays; 6 | 7 | import org.springframework.test.context.ContextConfiguration; 8 | import org.springframework.test.context.jdbc.Sql; 9 | import org.springframework.test.context.jdbc.SqlConfig; 10 | 11 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 12 | import com.github.marschall.storedprocedureproxy.configuration.FirebirdConfiguration; 13 | import com.github.marschall.storedprocedureproxy.procedures.FirebirdProcedures; 14 | 15 | 16 | @ContextConfiguration(classes = FirebirdConfiguration.class) 17 | @Sql(scripts = "classpath:sql/firebird_procedures.sql", config = @SqlConfig(separator = "^")) 18 | @DisabledOnTravis 19 | public class FirebirdTest extends AbstractDataSourceTest { 20 | 21 | private FirebirdProcedures procedures(ParameterRegistration parameterRegistration) { 22 | return ProcedureCallerFactory.of(FirebirdProcedures.class, this.getDataSource()) 23 | .withParameterRegistration(parameterRegistration) 24 | .build(); 25 | } 26 | 27 | @IndexedParametersRegistrationTest 28 | public void increment(ParameterRegistration parameterRegistration) { 29 | assertEquals(2, this.procedures(parameterRegistration).increment(1)); 30 | } 31 | 32 | @IndexedParametersRegistrationTest 33 | public void incrementOutParameter(ParameterRegistration parameterRegistration) { 34 | assertEquals(2, this.procedures(parameterRegistration).incrementOutParameter(1)); 35 | } 36 | 37 | @IndexedParametersRegistrationTest 38 | public void incrementReturnValue(ParameterRegistration parameterRegistration) { 39 | assertEquals(2, this.procedures(parameterRegistration).incrementReturnValue(1)); 40 | } 41 | 42 | @IndexedParametersRegistrationTest 43 | public void cursor(ParameterRegistration parameterRegistration) { 44 | assertEquals(Arrays.asList(1, 1, 2, 6, 24, 120), this.procedures(parameterRegistration).factorial(5)); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/H2IT.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.hasSize; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | 8 | import java.util.List; 9 | 10 | import org.springframework.test.context.ContextConfiguration; 11 | 12 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 13 | import com.github.marschall.storedprocedureproxy.configuration.H2Configuration; 14 | import com.github.marschall.storedprocedureproxy.procedures.H2Procedures; 15 | import com.github.marschall.storedprocedureproxy.procedures.H2Procedures.IdName; 16 | import com.github.marschall.storedprocedureproxy.spi.NamingStrategy; 17 | 18 | @ContextConfiguration(classes = H2Configuration.class) 19 | public class H2IT extends AbstractDataSourceTest { 20 | 21 | private H2Procedures procedures(ParameterRegistration parameterRegistration) { 22 | return ProcedureCallerFactory.of(H2Procedures.class, this.getDataSource()) 23 | .withProcedureNamingStrategy(NamingStrategy.snakeCase().thenUpperCase()) 24 | .withParameterRegistration(parameterRegistration) 25 | .build(); 26 | } 27 | 28 | @IndexedParametersRegistrationTest 29 | public void simpleResultSetWithDefaultMethod(ParameterRegistration parameterRegistration) { 30 | if (JavaVersionSupport.isJava9OrLater()) { 31 | List names = this.procedures(parameterRegistration) 32 | .simpleResultSet(); 33 | assertThat(names, hasSize(2)); 34 | IdName name = names.get(0); 35 | assertEquals(0L, name.getId()); 36 | assertEquals("Hello", name.getName()); 37 | name = names.get(1); 38 | assertEquals(1L, name.getId()); 39 | assertEquals("World", name.getName()); 40 | } else { 41 | assertThrows(IllegalStateException.class, () -> { 42 | this.procedures(parameterRegistration) 43 | .simpleResultSet(); 44 | }); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/DefaultMethodTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertThrows; 4 | import static org.junit.jupiter.api.Assumptions.assumeFalse; 5 | import static org.mockito.ArgumentMatchers.anyString; 6 | import static org.mockito.Mockito.mock; 7 | import static org.mockito.Mockito.when; 8 | 9 | import java.sql.CallableStatement; 10 | import java.sql.Connection; 11 | import java.sql.DatabaseMetaData; 12 | import java.sql.SQLException; 13 | 14 | import javax.sql.DataSource; 15 | 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | 19 | public class DefaultMethodTest { 20 | 21 | private DataSource dataSource; 22 | 23 | private Connection connection; 24 | 25 | @BeforeEach 26 | public void setUp() throws SQLException { 27 | this.dataSource = mock(DataSource.class); 28 | this.connection = mock(Connection.class); 29 | DatabaseMetaData metaData = mock(DatabaseMetaData.class); 30 | CallableStatement statement = mock(CallableStatement.class); 31 | 32 | when(this.dataSource.getConnection()).thenReturn(this.connection); 33 | when(this.connection.getMetaData()).thenReturn(metaData); 34 | when(metaData.getDatabaseProductName()).thenReturn("junit"); 35 | when(this.connection.prepareCall(anyString())).thenReturn(statement); 36 | when(statement.execute()).thenReturn(false); 37 | } 38 | 39 | 40 | 41 | @Test 42 | public void defaultMethod() throws SQLException { 43 | assumeFalse(isJava9OrLater()); 44 | DefaultMethod defaultMethod = ProcedureCallerFactory.build(DefaultMethod.class, this.dataSource); 45 | 46 | assertThrows(IllegalStateException.class, () -> defaultMethod.hello()); 47 | } 48 | 49 | 50 | 51 | private static boolean isJava9OrLater() { 52 | try { 53 | Class.forName("java.lang.Runtime$Version"); 54 | return true; 55 | } catch (ClassNotFoundException e) { 56 | return false; 57 | } 58 | } 59 | 60 | interface DefaultMethod { 61 | 62 | default String hello() { 63 | return "world"; 64 | } 65 | 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/spi/NamingStrategyTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.spi; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | public class NamingStrategyTest { 8 | 9 | @Test 10 | public void spBlitz() { 11 | NamingStrategy strategy = NamingStrategy.capitalize() 12 | .thenPrefix("sp_"); 13 | assertEquals("sp_Blitz", strategy.translateToDatabase("blitz")); 14 | } 15 | 16 | @Test 17 | public void snakeCase() { 18 | NamingStrategy strategy = NamingStrategy.snakeCase() 19 | .thenUpperCase(); 20 | assertEquals("JAVA_NAME", strategy.translateToDatabase("javaName")); 21 | } 22 | 23 | @Test 24 | public void lowerCase() { 25 | NamingStrategy strategy = NamingStrategy.snakeCase() 26 | .thenLowerCase(); 27 | assertEquals("java_name", strategy.translateToDatabase("javaName")); 28 | } 29 | 30 | @Test 31 | public void thenCapitalize() { 32 | NamingStrategy strategy = NamingStrategy.prefix("sp_") 33 | .thenCapitalize(); 34 | assertEquals("Sp_blitz", strategy.translateToDatabase("blitz")); 35 | 36 | } 37 | 38 | @Test 39 | public void capitalize() { 40 | NamingStrategy strategy = NamingStrategy.capitalize(); 41 | assertEquals("Java", strategy.translateToDatabase("java")); 42 | assertEquals("", strategy.translateToDatabase("")); 43 | assertEquals("1", strategy.translateToDatabase("1")); 44 | } 45 | 46 | @Test 47 | public void withoutFirst() { 48 | NamingStrategy strategy = NamingStrategy.withoutFirst(2); 49 | assertEquals("javaName", strategy.translateToDatabase("x_javaName")); 50 | } 51 | 52 | @Test 53 | public void compound() { 54 | NamingStrategy strategy = NamingStrategy.withoutFirst(2) 55 | .thenSnakeCase() 56 | .thenLowerCase(); 57 | assertEquals("java_name", strategy.translateToDatabase("x_javaName")); 58 | } 59 | 60 | @Test 61 | public void moreCompound() { 62 | NamingStrategy strategy = NamingStrategy.snakeCase() 63 | .thenUpperCase() 64 | .thenWithoutFirst(2); 65 | assertEquals("JAVA_NAME", strategy.translateToDatabase("xJavaName")); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/annotations/ReturnValue.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.annotations; 2 | 3 | import static java.lang.annotation.ElementType.METHOD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 11 | import com.github.marschall.storedprocedureproxy.spi.TypeMapper; 12 | 13 | /** 14 | * Signals that the procedure uses a return value rather than an out 15 | * parameter or result set. 16 | * 17 | *

Also allows to provide additional information about the return value.

18 | * 19 | *

You would use this for functions as opposed to procedures.

20 | * 21 | *

Causes a call string to be generated in the form of 22 | * {@code "{ ? = call function_name()}"} instead of {@code "{call function_name(?)}"}.

23 | * 24 | * @see OutParameter 25 | * @see InOutParameter 26 | * @see Return Value Result Extraction 27 | */ 28 | @Documented 29 | @Retention(RUNTIME) 30 | @Target(METHOD) 31 | public @interface ReturnValue { 32 | 33 | /** 34 | * Defines the SQL type of the return value. If nothing is specified 35 | * the default from {@link TypeMapper} is used. 36 | * 37 | * @return the return value SQL type, can be a vendor type 38 | * @see java.sql.Types 39 | */ 40 | int type() default Integer.MIN_VALUE; 41 | 42 | /** 43 | * Defines the name of the return value. Only used if the parameter 44 | * registration is either {@link ParameterRegistration#NAME_ONLY} or 45 | * {@link ParameterRegistration#NAME_AND_TYPE}. 46 | * 47 | * @return the name of the return value 48 | */ 49 | String name() default ""; 50 | 51 | /** 52 | * If the out parameter is a a user defined type like a named array 53 | * types then this method denotes the type name. 54 | * 55 | * @return the fully-qualified name of an SQL structured type 56 | * @see java.sql.CallableStatement#registerOutParameter(int, int, String) 57 | */ 58 | String typeName() default ""; 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/HsqlTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import java.sql.Timestamp; 7 | import java.time.LocalDateTime; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import org.springframework.test.context.ContextConfiguration; 12 | 13 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 14 | import com.github.marschall.storedprocedureproxy.configuration.HsqlConfiguration; 15 | import com.github.marschall.storedprocedureproxy.procedures.HsqlProcedures; 16 | 17 | @ContextConfiguration(classes = HsqlConfiguration.class) 18 | public class HsqlTest extends AbstractDataSourceTest { 19 | 20 | private HsqlProcedures procedures(ParameterRegistration parameterRegistration) { 21 | return ProcedureCallerFactory.of(HsqlProcedures.class, this.getDataSource()) 22 | .withParameterRegistration(parameterRegistration) 23 | .build(); 24 | } 25 | 26 | @IndexedParametersRegistrationTest 27 | public void function(ParameterRegistration parameterRegistration) { 28 | LocalDateTime after = LocalDateTime.of(2016, 10, 12, 17, 19); 29 | LocalDateTime before = after.minusHours(1L); 30 | assertEquals(Timestamp.valueOf(before), this.procedures(parameterRegistration).anHourBefore(Timestamp.valueOf(after))); 31 | } 32 | 33 | @IndexedParametersRegistrationTest 34 | public void refCursor(ParameterRegistration parameterRegistration) { 35 | List list = this.procedures(parameterRegistration).refCursor(); 36 | assertEquals(Arrays.asList(1, 2), list); 37 | } 38 | 39 | @IndexedParametersRegistrationTest 40 | public void arrayCardinality(ParameterRegistration parameterRegistration) { 41 | Integer[] array = new Integer[] {1, 2, 3}; 42 | int arrayCardinality = this.procedures(parameterRegistration).arrayCardinality(array); 43 | assertEquals(array.length, arrayCardinality); 44 | } 45 | 46 | @IndexedParametersRegistrationTest 47 | public void returnArray(ParameterRegistration parameterRegistration) { 48 | Integer[] actual = this.procedures(parameterRegistration).returnArray(); 49 | Integer[] expected = new Integer[] {0, 5, 10}; 50 | assertArrayEquals(expected, actual); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/test/resources/sql/postgres_procedures.sql: -------------------------------------------------------------------------------- 1 | -- use @ as a separator so Spring JDBC can deduce the end of a procedure definition 2 | -- https://www.postgresql.org/docs/9.5/static/plpgsql-declarations.html 3 | 4 | CREATE OR REPLACE FUNCTION sales_tax(subtotal real) 5 | RETURNS real AS $$ 6 | BEGIN 7 | RETURN subtotal * 0.06; 8 | END; 9 | $$ LANGUAGE plpgsql@ 10 | 11 | CREATE OR REPLACE FUNCTION property_tax(subtotal real, OUT tax real) AS $$ 12 | BEGIN 13 | tax := subtotal * 0.06; 14 | END; 15 | $$ LANGUAGE plpgsql@ 16 | 17 | 18 | -- https://www.postgresql.org/docs/9.5/static/plpgsql-porting.html 19 | 20 | CREATE OR REPLACE FUNCTION cs_fmt_browser_version(v_name varchar, 21 | v_version varchar) 22 | RETURNS varchar AS $$ 23 | BEGIN 24 | IF v_version IS NULL THEN 25 | RETURN v_name; 26 | END IF; 27 | RETURN v_name || '/' || v_version; 28 | END; 29 | $$ LANGUAGE plpgsql@ 30 | 31 | CREATE OR REPLACE FUNCTION raise_exception() 32 | RETURNS void AS $$ 33 | BEGIN 34 | RAISE SQLSTATE '22000'; 35 | END; 36 | $$ LANGUAGE plpgsql@ 37 | 38 | -- https://www.postgresql.org/docs/9.6/static/plpgsql-cursors.html 39 | 40 | CREATE OR REPLACE FUNCTION simple_ref_cursor() RETURNS refcursor AS $$ 41 | DECLARE 42 | ref refcursor; 43 | BEGIN 44 | OPEN ref FOR SELECT 'hello' UNION ALL SELECT 'postgres'; 45 | RETURN ref; 46 | END; 47 | $$ LANGUAGE plpgsql@ 48 | 49 | CREATE OR REPLACE FUNCTION ref_cursor_and_argument(v_prefix varchar) RETURNS refcursor AS $$ 50 | DECLARE 51 | ref refcursor; 52 | BEGIN 53 | OPEN ref FOR SELECT v_prefix || 'hello' UNION ALL SELECT v_prefix || 'postgres'; 54 | RETURN ref; 55 | END; 56 | $$ LANGUAGE plpgsql@ 57 | 58 | CREATE OR REPLACE FUNCTION simple_ref_cursor_out(OUT o_strings refcursor) AS $$ 59 | BEGIN 60 | OPEN o_strings FOR SELECT 'hello' UNION ALL SELECT 'postgres'; 61 | END; 62 | $$ LANGUAGE plpgsql@ 63 | 64 | CREATE OR REPLACE FUNCTION sample_array_argument(input_values int[]) 65 | RETURNS varchar AS $$ 66 | BEGIN 67 | return array_to_string(input_values, ', '); 68 | END; 69 | $$ LANGUAGE plpgsql@ 70 | 71 | CREATE OR REPLACE FUNCTION array_return_value() 72 | RETURNS int[] AS $$ 73 | BEGIN 74 | return ARRAY [1,2,3,4]; 75 | END; 76 | $$ LANGUAGE plpgsql@ 77 | 78 | CREATE OR REPLACE FUNCTION concatenate_two_arrays(first int[], second int[]) 79 | RETURNS int[] AS $$ 80 | BEGIN 81 | return array_cat(first, second); 82 | END; 83 | $$ LANGUAGE plpgsql@ 84 | 85 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/AnnotationBasedTypeNameResolverTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNull; 5 | 6 | import java.lang.reflect.Method; 7 | import java.lang.reflect.Parameter; 8 | import java.util.List; 9 | 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | 13 | import com.github.marschall.storedprocedureproxy.annotations.TypeName; 14 | import com.github.marschall.storedprocedureproxy.spi.TypeNameResolver; 15 | 16 | public class AnnotationBasedTypeNameResolverTest { 17 | 18 | private TypeNameResolver resolver; 19 | 20 | @BeforeEach 21 | public void setUp() { 22 | this.resolver = AnnotationBasedTypeNameResolver.INSTANCE; 23 | } 24 | 25 | @Test 26 | public void typeNameOnType() throws ReflectiveOperationException { 27 | Method method = SampleInterface.class.getDeclaredMethod("typeNameOnType", List.class); 28 | Parameter parameter = method.getParameters()[0]; 29 | assertEquals("typeName1", this.resolver.resolveTypeName(parameter)); 30 | } 31 | 32 | @Test 33 | public void typeNameOnParameter() throws ReflectiveOperationException { 34 | Method method = SampleInterface.class.getDeclaredMethod("typeNameOnParameter", List.class); 35 | Parameter parameter = method.getParameters()[0]; 36 | assertEquals("typeName2", this.resolver.resolveTypeName(parameter)); 37 | } 38 | 39 | @Test 40 | public void typeNameOnArray() throws ReflectiveOperationException { 41 | Method method = SampleInterface.class.getDeclaredMethod("typeNameOnArray", Integer[].class); 42 | Parameter parameter = method.getParameters()[0]; 43 | assertEquals("typeName3", this.resolver.resolveTypeName(parameter)); 44 | } 45 | 46 | @Test 47 | public void noTypeName() throws ReflectiveOperationException { 48 | Method method = SampleInterface.class.getDeclaredMethod("noTypeName", Integer.class); 49 | Parameter parameter = method.getParameters()[0]; 50 | assertNull(this.resolver.resolveTypeName(parameter)); 51 | } 52 | 53 | interface SampleInterface { 54 | 55 | void typeNameOnType(@TypeName("typeName1") List ids); 56 | 57 | void typeNameOnParameter(List<@TypeName("typeName2") Integer> ids); 58 | 59 | void typeNameOnArray(@TypeName("typeName3") Integer[] ids); 60 | 61 | void noTypeName(Integer id); 62 | 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ValueExtractorResultExtractorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.mockito.ArgumentMatchers.anyString; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.times; 7 | import static org.mockito.Mockito.verify; 8 | import static org.mockito.Mockito.when; 9 | 10 | import java.sql.CallableStatement; 11 | import java.sql.Connection; 12 | import java.sql.DatabaseMetaData; 13 | import java.sql.ResultSet; 14 | import java.sql.SQLException; 15 | import java.util.List; 16 | 17 | import javax.sql.DataSource; 18 | 19 | import org.junit.jupiter.api.Test; 20 | 21 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ProcedureCaller; 22 | 23 | public class ValueExtractorResultExtractorTest { 24 | 25 | @Test 26 | public void valueExtractorArguments() throws SQLException { 27 | // set up 28 | DataSource dataSource = mock(DataSource.class); 29 | CallableStatement callableStatement = mock(CallableStatement.class); 30 | Connection connection = mock(Connection.class); 31 | DatabaseMetaData metaData = mock(DatabaseMetaData.class); 32 | ResultSet resultSet = mock(ResultSet.class); 33 | 34 | when(dataSource.getConnection()).thenReturn(connection); 35 | when(connection.getMetaData()).thenReturn(metaData); 36 | when(metaData.getDatabaseProductName()).thenReturn("junit"); 37 | when(connection.prepareCall(anyString())).thenReturn(callableStatement); 38 | when(callableStatement.execute()).thenReturn(true); 39 | when(callableStatement.getResultSet()).thenReturn(resultSet); 40 | 41 | ValueExtractor valueExtractor = mock(ValueExtractor.class); 42 | SampleInterface procedures = ProcedureCallerFactory.build(SampleInterface.class, dataSource); 43 | 44 | // actual behavior 45 | when(resultSet.next()).thenReturn(true, true, false); 46 | when(valueExtractor.extractValue(resultSet)).thenReturn("s"); 47 | 48 | // when 49 | procedures.extractString(valueExtractor); 50 | 51 | // then 52 | verify(valueExtractor, times(2)).extractValue(resultSet); 53 | } 54 | 55 | @Test 56 | public void testToString() { 57 | ResultExtractor extractor = new ValueExtractorResultExtractor(1, ProcedureCaller.DEFAULT_FETCH_SIZE); 58 | assertEquals("ValueExtractorResultExtractor[methodParameterIndex=1, fetchSize=default]", extractor.toString()); 59 | 60 | extractor = new ValueExtractorResultExtractor(1, 10); 61 | assertEquals("ValueExtractorResultExtractor[methodParameterIndex=1, fetchSize=10]", extractor.toString()); 62 | } 63 | 64 | interface SampleInterface { 65 | 66 | List extractString(ValueExtractor valueExtractor); 67 | 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/annotations/OutParameter.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.annotations; 2 | 3 | import static java.lang.annotation.ElementType.METHOD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import java.lang.annotation.Documented; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 11 | import com.github.marschall.storedprocedureproxy.spi.TypeMapper; 12 | 13 | /** 14 | * Signals that the procedure uses an out parameter rather than a 15 | * return value or result set. 16 | * 17 | *

Applied to a method means that the return value should be retrieved using an 18 | * out parameter. 19 | * In addition allows to provide additional information about the out parameter.

20 | * 21 | *

Causes a call string to be generated in the form of 22 | * {@code {@code "{call function_name(?)}" instead of "{ ? = call function_name()}"}} 23 | * where one of the function arguments is an out parameter.

24 | * 25 | * @see ReturnValue 26 | * @see InOutParameter 27 | * @see Out Parameter Result Extraction 28 | */ 29 | @Documented 30 | @Retention(RUNTIME) 31 | @Target(METHOD) 32 | public @interface OutParameter { 33 | 34 | /** 35 | * Defines the SQL type of the out parameter. If nothing is specified 36 | * the default from {@link TypeMapper} is used. 37 | * 38 | * @return the out parameter SQL type, can be a vendor type 39 | * @see java.sql.Types 40 | */ 41 | int type() default Integer.MIN_VALUE; 42 | 43 | /** 44 | * Defines the index of the out parameter. If not specified the out 45 | * parameter is assumed to be the last parameter. 46 | * 47 | *

If the out parameter isn't the last parameter you have to 48 | * provide the index of the out parameter.

49 | * 50 | * @return the 1 based index of the out parameter 51 | */ 52 | int index() default -1; 53 | 54 | /** 55 | * Defines the name of the out parameter. Only used if the parameter 56 | * registration is either {@link ParameterRegistration#NAME_ONLY} or 57 | * {@link ParameterRegistration#NAME_AND_TYPE}. 58 | * 59 | * @return the name of the out parameter 60 | */ 61 | String name() default ""; 62 | 63 | /** 64 | * If the out parameter is a a user defined type like a named array 65 | * types then this method denotes the type name. 66 | * 67 | * @return the fully-qualified name of an SQL structured type 68 | * @see java.sql.CallableStatement#registerOutParameter(int, int, String) 69 | */ 70 | String typeName() default ""; 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ByNameAndTypeNameOutParameterRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.mockito.ArgumentMatchers.anyString; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.verify; 7 | import static org.mockito.Mockito.when; 8 | 9 | import java.sql.CallableStatement; 10 | import java.sql.Connection; 11 | import java.sql.DatabaseMetaData; 12 | import java.sql.SQLException; 13 | import java.sql.Types; 14 | 15 | import javax.sql.DataSource; 16 | 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.Test; 19 | 20 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 21 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 22 | 23 | public class ByNameAndTypeNameOutParameterRegistrationTest { 24 | 25 | private CallableStatement callableStatement; 26 | 27 | private ReturnTypeNameUser procedures; 28 | 29 | @BeforeEach 30 | public void setUp() throws SQLException { 31 | DataSource dataSource = mock(DataSource.class); 32 | this.callableStatement = mock(CallableStatement.class); 33 | Connection connection = mock(Connection.class); 34 | DatabaseMetaData metaData = mock(DatabaseMetaData.class); 35 | 36 | when(dataSource.getConnection()).thenReturn(connection); 37 | when(connection.getMetaData()).thenReturn(metaData); 38 | when(metaData.getDatabaseProductName()).thenReturn("junit"); 39 | when(connection.prepareCall(anyString())).thenReturn(this.callableStatement); 40 | 41 | this.procedures = ProcedureCallerFactory.of(ReturnTypeNameUser.class, dataSource) 42 | .withParameterRegistration(ParameterRegistration.NAME_ONLY) 43 | .build(); 44 | } 45 | 46 | @Test 47 | public void returnTypeNameOutParameter() throws SQLException { 48 | this.procedures.returnTypeNameOutParameter(); 49 | verify(this.callableStatement).registerOutParameter("out", Types.VARCHAR, "duck"); 50 | } 51 | 52 | @Test 53 | public void noReturnTypeNameOutParameter() throws SQLException { 54 | this.procedures.noReturnTypeNameOutParameter(); 55 | verify(this.callableStatement).registerOutParameter("out", Types.VARCHAR); 56 | } 57 | 58 | @Test 59 | public void testToString() { 60 | OutParameterRegistration registration = new ByNameAndTypeNameOutParameterRegistration("dog", Types.INTEGER, "duck"); 61 | assertEquals("ByNameAndTypeNameOutParameterRegistration[name=dog, type=4, typeName=duck]", registration.toString()); 62 | } 63 | 64 | interface ReturnTypeNameUser { 65 | 66 | @OutParameter(name = "out", typeName = "duck") 67 | String returnTypeNameOutParameter(); 68 | 69 | @OutParameter(name = "out") 70 | String noReturnTypeNameOutParameter(); 71 | 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/procedures/PostgresProcedures.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.procedures; 2 | 3 | import java.sql.SQLException; 4 | import java.util.List; 5 | 6 | import com.github.marschall.storedprocedureproxy.NumberedValueExtractor; 7 | import com.github.marschall.storedprocedureproxy.ValueExtractor; 8 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 9 | import com.github.marschall.storedprocedureproxy.annotations.ProcedureName; 10 | import com.github.marschall.storedprocedureproxy.annotations.ReturnValue; 11 | import com.github.marschall.storedprocedureproxy.annotations.TypeName; 12 | 13 | public interface PostgresProcedures { 14 | 15 | @ProcedureName("cs_fmt_browser_version") 16 | @ReturnValue 17 | String browserVersion(String name, String version); 18 | 19 | @ProcedureName("sales_tax") 20 | @ReturnValue 21 | float salesTax(float subtotal); 22 | 23 | @ProcedureName("property_tax") 24 | @OutParameter 25 | float propertyTax(float subtotal); 26 | 27 | @ProcedureName("raise_exception") 28 | void raiseCheckedException() throws SQLException; 29 | 30 | @ProcedureName("raise_exception") 31 | void raiseUncheckedException(); 32 | 33 | @ReturnValue 34 | @ProcedureName("simple_ref_cursor") 35 | List simpleRefCursor(); 36 | 37 | @OutParameter 38 | @ProcedureName("simple_ref_cursor_out") 39 | List simpleRefCursorOut(); 40 | 41 | @OutParameter 42 | @ProcedureName("simple_ref_cursor") 43 | List mappedRefCursor(NumberedValueExtractor extractor); 44 | 45 | @OutParameter 46 | @ProcedureName("simple_ref_cursor") 47 | List mappedRefCursor(ValueExtractor extractor); 48 | 49 | @OutParameter 50 | @ProcedureName("ref_cursor_and_argument") 51 | List mappedRefCursorAndArgument(String prefix, NumberedValueExtractor extractor); 52 | 53 | @OutParameter 54 | @ProcedureName("ref_cursor_and_argument") 55 | List mappedRefCursorAndArgument(String prefix, ValueExtractor extractor); 56 | 57 | @ReturnValue 58 | @ProcedureName("sample_array_argument") 59 | String sampleArrayArgumentList(@TypeName("int") List ids); 60 | 61 | @ReturnValue 62 | @ProcedureName("sample_array_argument") 63 | String sampleArrayArgumentTypeParameter(List<@TypeName("INTEGER") Integer> ids); 64 | 65 | @ReturnValue 66 | @ProcedureName("sample_array_argument") 67 | String sampleArrayArgumentArray(@TypeName("INTEGER") Integer[] ids); 68 | 69 | @ReturnValue 70 | @ProcedureName("sample_array_argument") 71 | String sampleArrayArgumentPrimitiveArray(int[] ids); 72 | 73 | @ReturnValue 74 | @ProcedureName("concatenate_two_arrays") 75 | Integer[] concatenateTwoArrays(Integer[] first, Integer[] second); 76 | 77 | @ReturnValue 78 | @ProcedureName("array_return_value") 79 | int[] arrayReturnValuePrimitive(); 80 | 81 | @ReturnValue 82 | @ProcedureName("array_return_value") 83 | Integer[] arrayReturnValueRefernce(); 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/NumberedValueExtractorResultExtractorTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.mockito.ArgumentMatchers.anyInt; 5 | import static org.mockito.ArgumentMatchers.anyString; 6 | import static org.mockito.ArgumentMatchers.eq; 7 | import static org.mockito.Mockito.mock; 8 | import static org.mockito.Mockito.times; 9 | import static org.mockito.Mockito.verify; 10 | import static org.mockito.Mockito.when; 11 | 12 | import java.sql.CallableStatement; 13 | import java.sql.Connection; 14 | import java.sql.DatabaseMetaData; 15 | import java.sql.ResultSet; 16 | import java.sql.SQLException; 17 | import java.util.List; 18 | 19 | import javax.sql.DataSource; 20 | 21 | import org.junit.jupiter.api.Test; 22 | 23 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ProcedureCaller; 24 | 25 | public class NumberedValueExtractorResultExtractorTest { 26 | 27 | @Test 28 | public void valueExtractorArguments() throws SQLException { 29 | // set up 30 | DataSource dataSource = mock(DataSource.class); 31 | CallableStatement callableStatement = mock(CallableStatement.class); 32 | Connection connection = mock(Connection.class); 33 | DatabaseMetaData metaData = mock(DatabaseMetaData.class); 34 | ResultSet resultSet = mock(ResultSet.class); 35 | 36 | when(dataSource.getConnection()).thenReturn(connection); 37 | when(connection.getMetaData()).thenReturn(metaData); 38 | when(metaData.getDatabaseProductName()).thenReturn("junit"); 39 | when(connection.prepareCall(anyString())).thenReturn(callableStatement); 40 | when(callableStatement.execute()).thenReturn(true); 41 | when(callableStatement.getResultSet()).thenReturn(resultSet); 42 | 43 | NumberedValueExtractor valueExtractor = mock(NumberedValueExtractor.class); 44 | SampleInterface procedures = ProcedureCallerFactory.build(SampleInterface.class, dataSource); 45 | 46 | // actual behavior 47 | when(resultSet.next()).thenReturn(true, true, false); 48 | when(valueExtractor.extractValue(eq(resultSet), anyInt())).thenReturn("s"); 49 | 50 | // when 51 | procedures.extractString(valueExtractor); 52 | 53 | // then 54 | verify(valueExtractor, times(1)).extractValue(resultSet, 0); 55 | verify(valueExtractor, times(1)).extractValue(resultSet, 1); 56 | } 57 | 58 | @Test 59 | public void testToString() { 60 | ResultExtractor extractor = new NumberedValueExtractorResultExtractor(1, ProcedureCaller.DEFAULT_FETCH_SIZE); 61 | assertEquals("NumberedValueExtractorResultExtractor[methodParameterIndex=1, fetchSize=default]", extractor.toString()); 62 | 63 | extractor = new NumberedValueExtractorResultExtractor(1, 10); 64 | assertEquals("NumberedValueExtractorResultExtractor[methodParameterIndex=1, fetchSize=10]", extractor.toString()); 65 | } 66 | 67 | interface SampleInterface { 68 | 69 | List extractString(NumberedValueExtractor valueExtractor); 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ProcedureCallerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | 6 | import org.junit.jupiter.api.Test; 7 | 8 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ProcedureCaller; 9 | 10 | public class ProcedureCallerTest { 11 | 12 | @Test 13 | public void buildQualifiedProcedureCallString() { 14 | assertEquals("{call p.n()}", ProcedureCaller.buildQualifiedProcedureCallString(null, "p", "n", 0)); 15 | assertEquals("{call p.n(?)}", ProcedureCaller.buildQualifiedProcedureCallString(null, "p", "n", 1)); 16 | assertEquals("{call p.n(?,?)}", ProcedureCaller.buildQualifiedProcedureCallString(null, "p", "n", 2)); 17 | } 18 | 19 | @Test 20 | public void buildSimpleProcudureCallString() { 21 | assertEquals("{call n()}", ProcedureCaller.buildQualifiedProcedureCallString(null, null, "n", 0)); 22 | assertEquals("{call n(?)}", ProcedureCaller.buildQualifiedProcedureCallString(null, null, "n", 1)); 23 | assertEquals("{call n(?,?)}", ProcedureCaller.buildQualifiedProcedureCallString(null, null, "n", 2)); 24 | } 25 | 26 | @Test 27 | public void buildQualifiedFunctionCallString() { 28 | assertEquals("{ ? = call p.n()}", ProcedureCaller.buildQualifiedFunctionCallString(null, "p", "n", 0)); 29 | assertEquals("{ ? = call p.n(?)}", ProcedureCaller.buildQualifiedFunctionCallString(null, "p", "n", 1)); 30 | assertEquals("{ ? = call p.n(?,?)}", ProcedureCaller.buildQualifiedFunctionCallString(null, "p", "n", 2)); 31 | } 32 | 33 | @Test 34 | public void buildSimpleFunctionCallString() { 35 | assertEquals("{ ? = call n()}", ProcedureCaller.buildQualifiedFunctionCallString(null, null, "n", 0)); 36 | assertEquals("{ ? = call n(?)}", ProcedureCaller.buildQualifiedFunctionCallString(null, null, "n", 1)); 37 | assertEquals("{ ? = call n(?,?)}", ProcedureCaller.buildQualifiedFunctionCallString(null, null, "n", 2)); 38 | } 39 | 40 | @Test 41 | public void buildInParameterIndicesNoOut() { 42 | assertArrayEquals(new byte[] {}, ProcedureCaller.buildInParameterIndices(0, new Class[0])); 43 | assertArrayEquals(new byte[] {1}, ProcedureCaller.buildInParameterIndices(1, new Class[] {String.class})); 44 | assertArrayEquals(new byte[] {1, 2}, ProcedureCaller.buildInParameterIndices(2, new Class[] {String.class, String.class})); 45 | } 46 | 47 | @Test 48 | public void buildInParameterIndicesWithOut() { 49 | assertArrayEquals(new byte[] {}, ProcedureCaller.buildInParameterIndices(0, 1, new Class[0])); 50 | assertArrayEquals(new byte[] {2, 3, 4}, ProcedureCaller.buildInParameterIndices(3, 1, new Class[] {String.class, String.class, String.class})); 51 | assertArrayEquals(new byte[] {1, 3, 4}, ProcedureCaller.buildInParameterIndices(3, 2, new Class[] {String.class, String.class, String.class})); 52 | assertArrayEquals(new byte[] {1, 2, 4}, ProcedureCaller.buildInParameterIndices(3, 3, new Class[] {String.class, String.class, String.class})); 53 | } 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/FetchSizeTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.mockito.ArgumentMatchers.anyInt; 4 | import static org.mockito.ArgumentMatchers.anyString; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.never; 7 | import static org.mockito.Mockito.verify; 8 | import static org.mockito.Mockito.when; 9 | 10 | import java.sql.CallableStatement; 11 | import java.sql.Connection; 12 | import java.sql.DatabaseMetaData; 13 | import java.sql.ResultSet; 14 | import java.sql.SQLException; 15 | import java.util.List; 16 | 17 | import javax.sql.DataSource; 18 | 19 | import org.junit.jupiter.api.BeforeEach; 20 | import org.junit.jupiter.api.Test; 21 | 22 | import com.github.marschall.storedprocedureproxy.annotations.FetchSize; 23 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 24 | 25 | public class FetchSizeTest { 26 | 27 | private DataSource dataSource; 28 | 29 | private CallableStatement statement; 30 | 31 | @BeforeEach 32 | public void setUp() throws SQLException { 33 | dataSource = mock(DataSource.class); 34 | Connection connection = mock(Connection.class); 35 | DatabaseMetaData metaData = mock(DatabaseMetaData.class); 36 | statement = mock(CallableStatement.class); 37 | ResultSet resultSet = mock(ResultSet.class); 38 | 39 | when(dataSource.getConnection()).thenReturn(connection); 40 | when(connection.getMetaData()).thenReturn(metaData); 41 | when(metaData.getDatabaseProductName()).thenReturn("junit"); 42 | when(connection.prepareCall(anyString())).thenReturn(statement); 43 | when(statement.execute()).thenReturn(false); 44 | when(statement.getObject(1, ResultSet.class)).thenReturn(resultSet); 45 | when(resultSet.next()).thenReturn(false); 46 | } 47 | 48 | @Test 49 | public void defaultFetchSize() throws SQLException { 50 | // given 51 | 52 | CustomFetchSize procedures = ProcedureCallerFactory.build(CustomFetchSize.class, dataSource); 53 | 54 | // when 55 | procedures.defaultFetchSize(); 56 | 57 | // then 58 | verify(statement).setFetchSize(10); 59 | } 60 | 61 | @Test 62 | public void customFetchSize() throws SQLException { 63 | // given 64 | 65 | CustomFetchSize procedures = ProcedureCallerFactory.build(CustomFetchSize.class, dataSource); 66 | 67 | // when 68 | procedures.customFetchSize(); 69 | 70 | // then 71 | verify(statement).setFetchSize(20); 72 | } 73 | 74 | @Test 75 | public void noFetchSize() throws SQLException { 76 | // given 77 | 78 | NoFetchSize procedures = ProcedureCallerFactory.build(NoFetchSize.class, dataSource); 79 | 80 | // when 81 | procedures.noFetchSize(); 82 | 83 | // then 84 | verify(statement, never()).setFetchSize(anyInt()); 85 | } 86 | 87 | @FetchSize(10) 88 | interface CustomFetchSize { 89 | 90 | @OutParameter 91 | List defaultFetchSize(); 92 | 93 | @FetchSize(20) 94 | @OutParameter 95 | List customFetchSize(); 96 | 97 | } 98 | 99 | interface NoFetchSize { 100 | 101 | @OutParameter 102 | List noFetchSize(); 103 | 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/ByIndexAndTypeNameOutParameterRegistrationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.mockito.ArgumentMatchers.anyString; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.verify; 7 | import static org.mockito.Mockito.when; 8 | 9 | import java.sql.CallableStatement; 10 | import java.sql.Connection; 11 | import java.sql.DatabaseMetaData; 12 | import java.sql.SQLException; 13 | import java.sql.Types; 14 | 15 | import javax.sql.DataSource; 16 | 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.Test; 19 | 20 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 21 | import com.github.marschall.storedprocedureproxy.annotations.ReturnValue; 22 | 23 | public class ByIndexAndTypeNameOutParameterRegistrationTest { 24 | 25 | private CallableStatement callableStatement; 26 | 27 | private ReturnTypeNameUser procedures; 28 | 29 | @BeforeEach 30 | public void setUp() throws SQLException { 31 | DataSource dataSource = mock(DataSource.class); 32 | this.callableStatement = mock(CallableStatement.class); 33 | Connection connection = mock(Connection.class); 34 | DatabaseMetaData metaData = mock(DatabaseMetaData.class); 35 | 36 | when(dataSource.getConnection()).thenReturn(connection); 37 | when(connection.getMetaData()).thenReturn(metaData); 38 | when(metaData.getDatabaseProductName()).thenReturn("junit"); 39 | when(connection.prepareCall(anyString())).thenReturn(this.callableStatement); 40 | 41 | this.procedures = ProcedureCallerFactory.build(ReturnTypeNameUser.class, dataSource); 42 | } 43 | 44 | @Test 45 | public void returnTypeNameOutParameter() throws SQLException { 46 | this.procedures.returnTypeNameOutParameter(); 47 | verify(this.callableStatement).registerOutParameter(1, Types.VARCHAR, "duck"); 48 | } 49 | 50 | @Test 51 | public void returnTypeNameFunction() throws SQLException { 52 | this.procedures.returnTypeNameFunction(); 53 | verify(this.callableStatement).registerOutParameter(1, Types.VARCHAR, "dog"); 54 | } 55 | 56 | @Test 57 | public void noReturnTypeNameOutParameter() throws SQLException { 58 | this.procedures.noReturnTypeNameOutParameter(); 59 | verify(this.callableStatement).registerOutParameter(1, Types.VARCHAR); 60 | } 61 | 62 | @Test 63 | public void noReturnTypeNameFunction() throws SQLException { 64 | this.procedures.noReturnTypeNameFunction(); 65 | verify(this.callableStatement).registerOutParameter(1, Types.VARCHAR); 66 | } 67 | 68 | @Test 69 | public void testToString() { 70 | OutParameterRegistration registration = new ByIndexAndTypeNameOutParameterRegistration(254, Types.INTEGER, "duck"); 71 | assertEquals("ByIndexAndTypeNameOutParameterRegistration[index=254, type=4, typeName=duck]", registration.toString()); 72 | } 73 | 74 | interface ReturnTypeNameUser { 75 | 76 | @OutParameter(typeName = "duck") 77 | String returnTypeNameOutParameter(); 78 | 79 | @ReturnValue(typeName = "dog") 80 | String returnTypeNameFunction(); 81 | 82 | @OutParameter 83 | String noReturnTypeNameOutParameter(); 84 | 85 | @ReturnValue 86 | String noReturnTypeNameFunction(); 87 | 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/DefaultTypeMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.math.BigDecimal; 4 | import java.math.BigInteger; 5 | import java.sql.Blob; 6 | import java.sql.Clob; 7 | import java.sql.NClob; 8 | import java.sql.SQLXML; 9 | import java.sql.Types; 10 | import java.time.LocalDate; 11 | import java.time.LocalDateTime; 12 | import java.time.LocalTime; 13 | import java.time.OffsetDateTime; 14 | import java.time.OffsetTime; 15 | import java.util.Collection; 16 | import java.util.HashMap; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Set; 20 | 21 | import com.github.marschall.storedprocedureproxy.spi.TypeMapper; 22 | 23 | final class DefaultTypeMapper implements TypeMapper { 24 | 25 | static final TypeMapper INSTANCE = new DefaultTypeMapper(); 26 | 27 | private final Map, Integer> typeMap; 28 | 29 | private DefaultTypeMapper() { 30 | this.typeMap = new HashMap<>(); 31 | this.typeMap.put(String.class, Types.VARCHAR); 32 | 33 | // char is not mapped 34 | 35 | // limited precision integers 36 | this.typeMap.put(Integer.class, Types.INTEGER); 37 | this.typeMap.put(int.class, Types.INTEGER); 38 | this.typeMap.put(Long.class, Types.BIGINT); 39 | this.typeMap.put(long.class, Types.BIGINT); 40 | this.typeMap.put(Short.class, Types.SMALLINT); 41 | this.typeMap.put(short.class, Types.SMALLINT); 42 | this.typeMap.put(Byte.class, Types.TINYINT); 43 | this.typeMap.put(byte.class, Types.TINYINT); 44 | // arbitrary precision numbers 45 | // should be an alias for DECIMAL but Oracle treats DECIMAL as double 46 | this.typeMap.put(BigDecimal.class, Types.NUMERIC); 47 | this.typeMap.put(BigInteger.class, Types.NUMERIC); 48 | 49 | // floating points 50 | this.typeMap.put(Float.class, Types.REAL); 51 | this.typeMap.put(float.class, Types.REAL); 52 | this.typeMap.put(Double.class, Types.DOUBLE); 53 | this.typeMap.put(double.class, Types.DOUBLE); 54 | 55 | // LOBs 56 | this.typeMap.put(Blob.class, Types.BLOB); 57 | this.typeMap.put(Clob.class, Types.CLOB); 58 | this.typeMap.put(NClob.class, Types.NCLOB); 59 | 60 | // java 8 date time 61 | this.typeMap.put(LocalDate.class, Types.DATE); 62 | this.typeMap.put(LocalTime.class, Types.TIME); 63 | this.typeMap.put(LocalDateTime.class, Types.TIMESTAMP); 64 | this.typeMap.put(OffsetTime.class, Types.TIME_WITH_TIMEZONE); 65 | this.typeMap.put(OffsetDateTime.class, Types.TIMESTAMP_WITH_TIMEZONE); 66 | 67 | // old date time 68 | this.typeMap.put(java.sql.Date.class, Types.DATE); 69 | this.typeMap.put(java.sql.Time.class, Types.TIME); 70 | this.typeMap.put(java.sql.Timestamp.class, Types.TIMESTAMP); 71 | 72 | this.typeMap.put(SQLXML.class, Types.SQLXML); 73 | // boolean 74 | this.typeMap.put(Boolean.class, Types.BOOLEAN); 75 | this.typeMap.put(boolean.class, Types.BOOLEAN); 76 | 77 | // array 78 | this.typeMap.put(Collection.class, Types.ARRAY); 79 | this.typeMap.put(Set.class, Types.ARRAY); 80 | this.typeMap.put(List.class, Types.ARRAY); 81 | } 82 | 83 | @Override 84 | public int mapToSqlType(Class javaType) { 85 | Integer sqlType = this.typeMap.get(javaType); 86 | if (sqlType == null) { 87 | if (javaType.isArray()) { 88 | return Types.ARRAY; 89 | } 90 | throw new IllegalArgumentException("unknown type: " + javaType); 91 | } 92 | return sqlType; 93 | } 94 | 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/OracleTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertFalse; 6 | 7 | import java.util.Arrays; 8 | 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.EnumSource; 11 | import org.springframework.test.context.ContextConfiguration; 12 | import org.springframework.test.context.jdbc.Sql; 13 | import org.springframework.test.context.jdbc.SqlConfig; 14 | import org.springframework.transaction.annotation.Transactional; 15 | 16 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 17 | import com.github.marschall.storedprocedureproxy.configuration.OracleConfiguration; 18 | import com.github.marschall.storedprocedureproxy.procedures.OraclePackageProcedures; 19 | import com.github.marschall.storedprocedureproxy.procedures.OracleProcedures; 20 | 21 | @DisabledOnTravis 22 | @Transactional 23 | @ContextConfiguration(classes = OracleConfiguration.class) 24 | @Sql(scripts = "classpath:sql/oracle_procedures.sql", config = @SqlConfig(separator = "/")) 25 | public class OracleTest extends AbstractDataSourceTest { 26 | 27 | private OracleProcedures procedures(ParameterRegistration parameterRegistration) { 28 | return ProcedureCallerFactory.of(OracleProcedures.class, this.getDataSource()) 29 | .withParameterRegistration(parameterRegistration) 30 | .withOracleExtensions() 31 | .build(); 32 | } 33 | 34 | private OraclePackageProcedures packageProcedures(ParameterRegistration parameterRegistration) { 35 | return ProcedureCallerFactory.of(OraclePackageProcedures.class, this.getDataSource()) 36 | .withParameterRegistration(parameterRegistration) 37 | .withNamespace() 38 | .withOracleExtensions() 39 | .build(); 40 | } 41 | 42 | @IndexedParametersRegistrationTest 43 | public void salesTax(ParameterRegistration parameterRegistration) { 44 | assertEquals(0.01f, 6.0f, this.procedures(parameterRegistration).salesTax(100.0f)); 45 | } 46 | 47 | @AllParametersRegistrationTest 48 | public void propertyTax(ParameterRegistration parameterRegistration) { 49 | assertEquals(0.01f, 6.0f, this.procedures(parameterRegistration).propertyTax(100.0f)); 50 | } 51 | 52 | @ParameterizedTest 53 | @EnumSource(value = ParameterRegistration.class, names = {"INDEX_AND_TYPE"}) 54 | public void booleanFunction(ParameterRegistration parameterRegistration) { 55 | assertFalse(this.packageProcedures(parameterRegistration).negateFunction(true)); 56 | } 57 | 58 | @ParameterizedTest 59 | @EnumSource(value = ParameterRegistration.class, names = {"INDEX_AND_TYPE", "NAME_AND_TYPE"}) 60 | public void booleanProcedure(ParameterRegistration parameterRegistration) { 61 | assertFalse(this.packageProcedures(parameterRegistration).negateProcedure(true)); 62 | } 63 | 64 | @AllParametersRegistrationTest 65 | public void arrayParameter(ParameterRegistration parameterRegistration) { 66 | assertEquals(6, this.packageProcedures(parameterRegistration).sum(new int[] {1, 2, 3})); 67 | } 68 | 69 | @AllParametersRegistrationTest 70 | public void arrayResult(ParameterRegistration parameterRegistration) { 71 | assertArrayEquals(new int[] {1, 2, 3}, this.packageProcedures(parameterRegistration).arrayResult()); 72 | } 73 | 74 | @ParameterizedTest 75 | @AllParametersRegistrationTest 76 | public void retrunRefCursor(ParameterRegistration parameterRegistration) { 77 | assertEquals(Arrays.asList(1, 2, 3), this.packageProcedures(parameterRegistration).returnRefcursor()); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/CompositeFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | import static org.mockito.Mockito.doThrow; 7 | import static org.mockito.Mockito.mock; 8 | import static org.mockito.Mockito.times; 9 | import static org.mockito.Mockito.verify; 10 | 11 | import java.sql.Connection; 12 | import java.sql.SQLException; 13 | 14 | import org.junit.jupiter.api.Test; 15 | 16 | public class CompositeFactoryTest { 17 | 18 | @Test 19 | public void exceptionDuringCreation() throws SQLException { 20 | // given 21 | String exceptionMessage = "Premium Bier"; 22 | String supressedMessage = "Club-Mate"; 23 | Connection connection = mock(Connection.class); 24 | CallResource throwOnCloseResource = mock(CallResource.class); 25 | CallResource noThrowResource = mock(CallResource.class); 26 | doThrow(new SQLException(supressedMessage)).when(throwOnCloseResource).close();; 27 | 28 | CallResourceFactory factory = new CompositeFactory(new CallResourceFactory[] { 29 | new DelegatingResourceFactory(throwOnCloseResource), 30 | new DelegatingResourceFactory(noThrowResource), 31 | new ThrowingResourceFactory(exceptionMessage) 32 | }); 33 | 34 | // when 35 | SQLException e = assertThrows(SQLException.class, () -> factory.createResource(connection, new Object[0])); 36 | assertEquals(exceptionMessage, e.getMessage()); 37 | Throwable[] suppressed = e.getSuppressed(); 38 | assertNotNull(suppressed); 39 | assertEquals(1, suppressed.length); 40 | assertEquals(supressedMessage, suppressed[0].getMessage()); 41 | 42 | // then 43 | verify(throwOnCloseResource, times(1)).close(); 44 | verify(noThrowResource, times(1)).close(); 45 | } 46 | 47 | @Test 48 | public void testToString() { 49 | CallResourceFactory factory1 = new ToStringResourceFactory("factory1"); 50 | CallResourceFactory factory2 = new ToStringResourceFactory("factory2"); 51 | CallResourceFactory composite = new CompositeFactory(new CallResourceFactory[] {factory1, factory2}); 52 | 53 | assertEquals("CompositeFactory[factory1, factory2]", composite.toString()); 54 | } 55 | 56 | static final class DelegatingResourceFactory implements CallResourceFactory { 57 | 58 | private final CallResource callResource; 59 | 60 | DelegatingResourceFactory(CallResource callResource) { 61 | this.callResource = callResource; 62 | } 63 | 64 | @Override 65 | public CallResource createResource(Connection connection, Object[] args) { 66 | return this.callResource; 67 | } 68 | 69 | } 70 | 71 | static final class ThrowingResourceFactory implements CallResourceFactory { 72 | 73 | private final String message; 74 | 75 | ThrowingResourceFactory(String message) { 76 | this.message = message; 77 | } 78 | 79 | @Override 80 | public CallResource createResource(Connection connection, Object[] args)throws SQLException { 81 | throw new SQLException(this.message); 82 | } 83 | 84 | } 85 | 86 | static final class ToStringResourceFactory implements CallResourceFactory { 87 | 88 | private final String s; 89 | 90 | ToStringResourceFactory(String s) { 91 | this.s = s; 92 | } 93 | 94 | @Override 95 | public CallResource createResource(Connection connection, Object[] args) { 96 | throw new IllegalStateException("should not be called"); 97 | } 98 | 99 | @Override 100 | public String toString() { 101 | return this.s; 102 | } 103 | 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/spi/TypeMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.spi; 2 | 3 | import java.sql.Types; 4 | 5 | import com.github.marschall.storedprocedureproxy.annotations.OutParameter; 6 | import com.github.marschall.storedprocedureproxy.annotations.ParameterType; 7 | import com.github.marschall.storedprocedureproxy.annotations.ReturnValue; 8 | 9 | /** 10 | * Maps a Java type to a SQL type. This is especially useful if you 11 | * want to use vendor types. 12 | * 13 | *

The mapping defined by an instance of this class will be applied 14 | * globally to all in and out parameter of all methods in an interface. 15 | * If you want to customize only a single parameter use 16 | * {@link ParameterType}, {@link OutParameter#type()} or 17 | * {@link ReturnValue#type()} instead.

18 | * 19 | *

If no custom implementation is specified the following default 20 | * are used:

21 | * 22 | * 23 | * 24 | * 25 | * 26 | * 27 | * 28 | * 29 | * 30 | * 31 | * 32 | * 33 | * 34 | * 35 | * 36 | * 37 | * 38 | * 39 | 40 | * 41 | * 42 | * 43 | * 44 | * 45 | 46 | * 47 | * 48 | * 49 | * 50 | 51 | * 52 | * 53 | * 54 | * 55 | * 56 | * 57 | 58 | * 59 | * 60 | * 61 | * 62 | 63 | * 64 | * 65 | * 66 | * 67 | * 68 | 69 | * 70 | * 71 | * 72 | * 73 | * 74 | *
default type mappings
Java TypeSQL type
String{@link Types#VARCHAR}
char is not mapped
limited precision integers
Integer{@link Types#INTEGER}
int{@link Types#INTEGER}
Long{@link Types#BIGINT}
long{@link Types#BIGINT}
Short{@link Types#SMALLINT}
short{@link Types#SMALLINT}
Byte{@link Types#TINYINT}
byte{@link Types#TINYINT}
arbitrary precision numbers
should be an alias for DECIMAL but Oracle treats DECIMAL as double
BigDecimal{@link Types#NUMERIC}
BigInteger{@link Types#NUMERIC}
floating points
Float{@link Types#REAL}
Double{@link Types#DOUBLE}
float{@link Types#REAL}
double{@link Types#DOUBLE}
LOBs
Blob{@link Types#BLOB}
Clob{@link Types#CLOB}
NClob{@link Types#NCLOB}
java 8 date time
LocalDate{@link Types#DATE}
LocalTime{@link Types#TIME}
LocalDateTime{@link Types#TIMESTAMP}
OffsetTime{@link Types#TIME_WITH_TIMEZONE}
OffsetDateTime{@link Types#TIMESTAMP_WITH_TIMEZONE}
old date time
java.sql.Date{@link Types#DATE}
java.sql.Time{@link Types#TIME}
java.sql.Timestamp{@link Types#TIMESTAMP}
XML
SQLXML{@link Types#SQLXML}
boolean
Boolean{@link Types#BOOLEAN}
boolean{@link Types#BOOLEAN}
ARRAY
Collection{@link Types#ARRAY}
List{@link Types#ARRAY}
Set{@link Types#ARRAY}
array{@link Types#ARRAY}
75 | */ 76 | @FunctionalInterface 77 | public interface TypeMapper { 78 | 79 | /** 80 | * Maps a Java type to a SQL type. 81 | * 82 | * @see java.sql.Types 83 | * @param javaType 84 | * the java type, may be a primitive type like {@code int.class}, 85 | * never {@code null}, never {@code void.class} 86 | * @return the SQL type, may be a vendor type 87 | */ 88 | int mapToSqlType(Class javaType); 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/DefaultTypeNameResolver.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.lang.reflect.Parameter; 4 | import java.lang.reflect.ParameterizedType; 5 | import java.lang.reflect.Type; 6 | import java.math.BigDecimal; 7 | import java.math.BigInteger; 8 | import java.time.LocalDate; 9 | import java.time.LocalDateTime; 10 | import java.time.LocalTime; 11 | import java.util.Collection; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | import com.github.marschall.storedprocedureproxy.spi.TypeNameResolver; 16 | 17 | /** 18 | * Default implementation of {@link TypeNameResolver}. 19 | * 20 | *

The behavior is:

21 | *
    22 | *
  1. if a parameter name is present use its value
  2. 23 | *
24 | * 25 | */ 26 | final class DefaultTypeNameResolver implements TypeNameResolver { 27 | 28 | static final TypeNameResolver INSTANCE = new DefaultTypeNameResolver(); 29 | 30 | private final Map, String> typeMap; 31 | 32 | private DefaultTypeNameResolver() { 33 | this.typeMap = new HashMap<>(); 34 | this.typeMap.put(String.class, "VARCHAR"); 35 | 36 | // char is not mapped 37 | 38 | // limited precision integers 39 | this.typeMap.put(Integer.class, "INTEGER"); 40 | this.typeMap.put(int.class, "INTEGER"); 41 | this.typeMap.put(Long.class, "BIGINT"); 42 | this.typeMap.put(long.class, "BIGINT"); 43 | this.typeMap.put(Short.class, "SMALLINT"); 44 | this.typeMap.put(short.class, "SMALLINT"); 45 | this.typeMap.put(Byte.class, "TINYINT"); 46 | this.typeMap.put(byte.class, "TINYINT"); 47 | // arbitrary precision numbers 48 | // should be an alias for DECIMAL but Oracle treats DECIMAL as double 49 | this.typeMap.put(BigDecimal.class, "NUMERIC"); 50 | this.typeMap.put(BigInteger.class, "NUMERIC"); 51 | 52 | // floating points 53 | this.typeMap.put(Float.class, "REAL"); 54 | this.typeMap.put(float.class, "REAL"); 55 | this.typeMap.put(Double.class, "DOUBLE"); 56 | this.typeMap.put(double.class, "DOUBLE"); 57 | 58 | // java 8 date time 59 | this.typeMap.put(LocalDate.class, "DATE"); 60 | this.typeMap.put(LocalTime.class, "TIME"); 61 | this.typeMap.put(LocalDateTime.class, "TIMESTAMP"); 62 | 63 | // old date time 64 | this.typeMap.put(java.sql.Date.class, "DATE"); 65 | this.typeMap.put(java.sql.Time.class, "TIME"); 66 | this.typeMap.put(java.sql.Timestamp.class, "TIMESTAMP"); 67 | 68 | // boolean 69 | this.typeMap.put(Boolean.class, "BOOLEAN"); 70 | this.typeMap.put(boolean.class, "BOOLEAN"); 71 | } 72 | 73 | @Override 74 | public String resolveTypeName(Parameter parameter) { 75 | Class parameterType = parameter.getType(); 76 | Class elementType; 77 | if (Collection.class.isAssignableFrom(parameterType)) { 78 | elementType = getCollectionTypeParameter(parameter); 79 | } else if (parameterType.isArray()) { 80 | elementType = parameterType.getComponentType(); 81 | } else { 82 | throw new IllegalArgumentException("parameter " + parameter + " needs to be List or array"); 83 | } 84 | 85 | String typeName = this.typeMap.get(elementType); 86 | if (typeName == null) { 87 | throw new IllegalArgumentException("SQL type for element type: " + elementType + " can not be determined"); 88 | } 89 | return typeName; 90 | } 91 | 92 | private Class getCollectionTypeParameter(Parameter parameter) { 93 | Type type = parameter.getParameterizedType(); 94 | if (type instanceof ParameterizedType) { 95 | ParameterizedType parameterizedType = (ParameterizedType) type; 96 | Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); 97 | if (actualTypeArguments.length != 1) { 98 | throw new IllegalArgumentException("type arguments of parameter " + parameter + " are missing"); 99 | } 100 | Type actualTypeArgument = actualTypeArguments[0]; 101 | if (!(actualTypeArgument instanceof Class)) { 102 | throw new IllegalArgumentException("type arguments of parameter" + parameter + " is not a class"); 103 | } 104 | return ((Class) actualTypeArgument); 105 | } else { 106 | throw new IllegalArgumentException("parameter " + parameter + " is missing type paramter for " + type); 107 | } 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/CompositeResourceTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | 7 | import java.sql.SQLException; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class CompositeResourceTest { 12 | 13 | @Test 14 | public void multipleExceptions() { 15 | CallResource resource1 = new ThrowingResource("message1"); 16 | CallResource resource2 = new ThrowingResource("message2"); 17 | CallResource composite = new CompositeResource(new CallResource[]{resource1, resource2}); 18 | 19 | SQLException e = assertThrows(SQLException.class, () -> composite.close()); 20 | assertEquals("message1", e.getMessage()); 21 | Throwable[] suppressed = e.getSuppressed(); 22 | assertNotNull(suppressed); 23 | assertEquals(1, suppressed.length); 24 | assertEquals("message2", suppressed[0].getMessage()); 25 | } 26 | 27 | @Test 28 | public void lastException() { 29 | CallResource resource1 = new NonThrowingResource(); 30 | CallResource resource2 = new ThrowingResource("message2"); 31 | CallResource composite = new CompositeResource(new CallResource[]{resource1, resource2}); 32 | 33 | SQLException e = assertThrows(SQLException.class, () -> composite.close()); 34 | assertEquals("message2", e.getMessage()); 35 | Throwable[] suppressed = e.getSuppressed(); 36 | assertNotNull(suppressed); 37 | assertEquals(0, suppressed.length); 38 | } 39 | 40 | @Test 41 | public void firstException() { 42 | CallResource resource1 = new ThrowingResource("message1"); 43 | CallResource resource2 = new NonThrowingResource(); 44 | CallResource composite = new CompositeResource(new CallResource[]{resource1, resource2}); 45 | 46 | SQLException e = assertThrows(SQLException.class, () -> composite.close()); 47 | assertEquals("message1", e.getMessage()); 48 | Throwable[] suppressed = e.getSuppressed(); 49 | assertNotNull(suppressed); 50 | assertEquals(0, suppressed.length); 51 | } 52 | 53 | @Test 54 | public void noException() throws SQLException { 55 | CallResource resource1 = new NonThrowingResource(); 56 | CallResource resource2 = new NonThrowingResource(); 57 | CallResource composite = new CompositeResource(new CallResource[]{resource1, resource2}); 58 | 59 | composite.close(); 60 | } 61 | 62 | @Test 63 | public void testToString() throws SQLException { 64 | CallResource resource1 = new ToStringResource("resource1"); 65 | CallResource resource2 = new ToStringResource("resource2"); 66 | 67 | try (CallResource composite = new CompositeResource(new CallResource[]{resource1, resource2})) { 68 | assertEquals("CompositeResource[resource1, resource2]", composite.toString()); 69 | } 70 | } 71 | 72 | static final class NonThrowingResource implements CallResource { 73 | 74 | 75 | @Override 76 | public boolean hasResourceAt(int index) { 77 | throw new IllegalStateException("should not be called"); 78 | } 79 | 80 | @Override 81 | public Object resourceAt(int index) { 82 | throw new IllegalStateException("should not be called"); 83 | } 84 | 85 | @Override 86 | public void close() throws SQLException { 87 | // nothing 88 | } 89 | 90 | } 91 | 92 | static final class ThrowingResource implements CallResource { 93 | 94 | private final String message; 95 | 96 | ThrowingResource(String message) { 97 | this.message = message; 98 | } 99 | 100 | @Override 101 | public boolean hasResourceAt(int index) { 102 | throw new IllegalStateException("should not be called"); 103 | } 104 | 105 | @Override 106 | public Object resourceAt(int index) { 107 | throw new IllegalStateException("should not be called"); 108 | } 109 | 110 | @Override 111 | public void close() throws SQLException { 112 | throw new SQLException(this.message); 113 | } 114 | 115 | } 116 | 117 | static final class ToStringResource implements CallResource { 118 | 119 | private final String s; 120 | 121 | ToStringResource(String s) { 122 | this.s = s; 123 | } 124 | 125 | @Override 126 | public boolean hasResourceAt(int index) { 127 | throw new IllegalStateException("should not be called"); 128 | } 129 | 130 | @Override 131 | public Object resourceAt(int index) { 132 | throw new IllegalStateException("should not be called"); 133 | } 134 | 135 | @Override 136 | public void close() throws SQLException { 137 | // ignore 138 | } 139 | 140 | @Override 141 | public String toString() { 142 | return this.s; 143 | } 144 | 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Stored Procedure Proxy [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.marschall/stored-procedure-proxy/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.marschall/stored-procedure-proxy) [![Javadocs](http://www.javadoc.io/badge/com.github.marschall/stored-procedure-proxy.svg)](http://www.javadoc.io/doc/com.github.marschall/stored-procedure-proxy) [![Build Status](https://app.travis-ci.com/marschall/stored-procedure-proxy.svg?branch=master)](https://app.travis-ci.com/marschall/stored-procedure-proxy) [![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)](https://opensource.org/licenses/MIT) 2 | ====================== 3 | 4 | A more convenient and type safe way to call stored procedures from Java. 5 | 6 | This project allows you to define a Java interface method for every stored procedure you want to call. Then it creates a dynamic instance of that interface that calls the stored procedure whenever you call the method. 7 | 8 | Simply create an interface that represents the stored procedures you want to call. 9 | 10 | ```java 11 | public interface TaxProcedures { 12 | 13 | BigDecimal salesTax(BigDecimal subtotal); 14 | 15 | } 16 | 17 | ``` 18 | 19 | Then create an instance using only a `javax.sql.DataSource` 20 | 21 | ```java 22 | TaxProcedures taxProcedures = ProcedureCallerFactory.build(TaxProcedures.class, dataSource); 23 | 24 | ``` 25 | 26 | Invoking interface methods will then call stored procedure. 27 | 28 | ```java 29 | taxProcedures.salesTax(new BigDecimal("100.00")); 30 | ``` 31 | 32 | will actually call the stored procedure. 33 | 34 | 35 | Check out the [wiki](https://github.com/marschall/stored-procedure-proxy/wiki) for more information. 36 | 37 | The project has no runtime dependencies and is a single JAR weighting 100 kB. 38 | 39 | ```xml 40 | 41 | com.github.marschall 42 | stored-procedure-proxy 43 | 0.12.0 44 | 45 | ``` 46 | 47 | 48 | What problem does this project solve? 49 | ------------------------------------- 50 | 51 | Calling simple stored procedures in JDBC or JPA is unnecessarily [cumbersome](https://blog.jooq.org/2016/06/08/using-stored-procedures-with-jpa-jdbc-meh-just-use-jooq/) and not type safe. While this may be required in rare cases in common cases this can be solved much easier. None of the common data access layers solve this issue: 52 | 53 | - While [spring-jdbc](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html) offers many ways to call a stored procedure all of them require the registration of [SqlParameter](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/SqlParameter.html) objects. The options are: 54 | - call [JdbcOperations#call](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/JdbcOperations.html#call-org.springframework.jdbc.core.CallableStatementCreator-java.util.List-) 55 | - sublcass [StoredProcedure](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/object/StoredProcedure.html) 56 | - use [GenericStoredProcedure](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/object/GenericStoredProcedure.html) 57 | - use [SimpleJdbcCall](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/simple/SimpleJdbcCall.html), accesses database metadata by default. Metadata access has to be [disabled](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/jdbc/core/simple/SimpleJdbcCall.html#withoutProcedureColumnMetaDataAccess--) for every use 58 | - [Spring Data JPA](https://github.com/spring-projects/spring-data-examples/tree/master/jpa/jpa21) offers two ways 59 | - the first is hardly an improvement since it still needs a `@NamedStoredProcedureQuery` 60 | - the [second](https://jira.spring.io/browse/DATAJPA-455) is quite nice, we take inspiration from this approach and add more flexibility 61 | - [jOOQ](https://www.jooq.org/doc/3.10/manual/sql-execution/stored-procedures/) offers stored procedure support in a way that is similar to this project, in addition it supports many more features and can generate classes from a database schema. The only down sides are that it requires passing a configuration object ([for now](https://github.com/jOOQ/jOOQ/issues/5677)) and Oracle support is commercial. 62 | - [jDBI](https://github.com/jdbi/jdbi/issues/135) falls back to manual parameter registration for out parameters as well. 63 | - [Ebean](https://ebean-orm.github.io/apidocs/com/avaje/ebean/CallableSql.html) falls back to manual parameter registration for out parameters as well. 64 | - [Querydsl](https://github.com/querydsl/querydsl/issues/15) has no support at all 65 | - [Sql2o](https://groups.google.com/forum/#!topic/sql2o/4Fdh5VjZ-uk) seems to have no support at all 66 | - [spwrap](https://github.com/mhewedy/spwrap) is similar in spirit but requires more annotations and is currently a bit less flexible 67 | 68 | While they all have their use case none of them fitted our needs. 69 | 70 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/H2Test.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.hasSize; 5 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 6 | import static org.junit.jupiter.api.Assertions.assertEquals; 7 | 8 | import java.sql.ResultSet; 9 | import java.sql.SQLException; 10 | import java.util.List; 11 | import java.util.function.Function; 12 | 13 | import org.junit.jupiter.api.Disabled; 14 | import org.springframework.test.context.ContextConfiguration; 15 | 16 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 17 | import com.github.marschall.storedprocedureproxy.configuration.H2Configuration; 18 | import com.github.marschall.storedprocedureproxy.procedures.H2Procedures; 19 | import com.github.marschall.storedprocedureproxy.procedures.H2Procedures.IdName; 20 | import com.github.marschall.storedprocedureproxy.spi.NamingStrategy; 21 | 22 | @ContextConfiguration(classes = H2Configuration.class) 23 | public class H2Test extends AbstractDataSourceTest { 24 | 25 | private H2Procedures procedures(ParameterRegistration parameterRegistration) { 26 | return ProcedureCallerFactory.of(H2Procedures.class, this.getDataSource()) 27 | .withProcedureNamingStrategy(NamingStrategy.snakeCase().thenUpperCase()) 28 | .withParameterRegistration(parameterRegistration) 29 | .build(); 30 | } 31 | 32 | @IndexedParametersRegistrationTest 33 | public void callScalarFunction(ParameterRegistration parameterRegistration) { 34 | String input = "test"; 35 | assertEquals("pre" + input + "post", this.procedures(parameterRegistration).stringProcedure(input)); 36 | } 37 | 38 | @IndexedParametersRegistrationTest 39 | public void callVoidProcedure(ParameterRegistration parameterRegistration) { 40 | this.procedures(parameterRegistration).voidProcedure("test"); 41 | } 42 | 43 | @IndexedParametersRegistrationTest 44 | public void noArgProcedure(ParameterRegistration parameterRegistration) { 45 | assertEquals("output", this.procedures(parameterRegistration).noArgProcedure()); 46 | } 47 | 48 | @IndexedParametersRegistrationTest 49 | public void reverseIntegerArray(ParameterRegistration parameterRegistration) { 50 | Integer[] input = new Integer[] {11, 2, 15}; 51 | Integer[] expected = new Integer[] {15, 2, 11}; 52 | assertArrayEquals(expected, this.procedures(parameterRegistration).reverseIntegerArray(input)); 53 | } 54 | 55 | @IndexedParametersRegistrationTest 56 | public void returnIntegerArray(ParameterRegistration parameterRegistration) { 57 | Integer[] expected = new Integer[] {4, 1, 7}; 58 | assertArrayEquals(expected, this.procedures(parameterRegistration).returnIntegerArray()); 59 | } 60 | 61 | @IndexedParametersRegistrationTest 62 | public void simpleResultSetNumbered(ParameterRegistration parameterRegistration) { 63 | List names = this.procedures(parameterRegistration).simpleResultSet((rs, i) -> { 64 | long id = rs.getLong("ID"); 65 | String name = rs.getString("NAME"); 66 | return new IdName(id, i + "-" + name); 67 | }); 68 | 69 | assertThat(names, hasSize(2)); 70 | 71 | IdName name = names.get(0); 72 | assertEquals(0L, name.getId()); 73 | assertEquals("0-Hello", name.getName()); 74 | 75 | name = names.get(1); 76 | assertEquals(1L, name.getId()); 77 | assertEquals("1-World", name.getName()); 78 | } 79 | 80 | @IndexedParametersRegistrationTest 81 | public void simpleResultSet(ParameterRegistration parameterRegistration) { 82 | List names = this.procedures(parameterRegistration).simpleResultSet((ValueExtractor) rs -> { 83 | long id = rs.getLong("ID"); 84 | String name = rs.getString("NAME"); 85 | return new IdName(id, name); 86 | }); 87 | 88 | assertThat(names, hasSize(2)); 89 | 90 | IdName name = names.get(0); 91 | assertEquals(0L, name.getId()); 92 | assertEquals("Hello", name.getName()); 93 | 94 | name = names.get(1); 95 | assertEquals(1L, name.getId()); 96 | assertEquals("World", name.getName()); 97 | } 98 | 99 | @IndexedParametersRegistrationTest 100 | @Disabled("feature not ready") 101 | public void simpleResultSetFunction(ParameterRegistration parameterRegistration) { 102 | List names = this.procedures(parameterRegistration).simpleResultSet((Function) rs -> { 103 | long id; 104 | String name; 105 | try { 106 | id = rs.getLong("ID"); 107 | name = rs.getString("NAME"); 108 | } catch (SQLException e) { 109 | throw new RuntimeException(e); 110 | } 111 | return new IdName(id, name); 112 | }); 113 | 114 | assertThat(names, hasSize(2)); 115 | 116 | IdName name = names.get(0); 117 | assertEquals(0L, name.getId()); 118 | assertEquals("Hello", name.getName()); 119 | 120 | name = names.get(1); 121 | assertEquals(1L, name.getId()); 122 | assertEquals("World", name.getName()); 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java9/com/github/marschall/storedprocedureproxy/Java9DefaultMethodSupport.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.invoke.MethodHandles.Lookup; 6 | import java.lang.invoke.MethodType; 7 | import java.lang.reflect.Method; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | import java.util.concurrent.locks.Lock; 12 | import java.util.concurrent.locks.ReadWriteLock; 13 | import java.util.concurrent.locks.ReentrantReadWriteLock; 14 | 15 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 16 | 17 | final class Java9DefaultMethodSupport implements DefaultMethodSupport { 18 | 19 | private static final MethodHandle CLASS_GETMODULE; 20 | 21 | private static final MethodHandle MODULE_ISNAMED; 22 | 23 | private static final MethodHandle MODULE_ADDREADS; 24 | 25 | private static final MethodHandle PRIVE_LOOKUP_IN; 26 | 27 | static { 28 | MethodHandle classGetModule; 29 | MethodHandle moduleIsNamed; 30 | MethodHandle moduleAddReads; 31 | MethodHandle privateLookupIn; 32 | try { 33 | Class moduleClass = Class.forName("java.lang.Module"); 34 | 35 | // java.lang.Class.getModule() 36 | MethodType returnsModule = MethodType.methodType(moduleClass); 37 | classGetModule = MethodHandles.publicLookup().findVirtual(Class.class, "getModule", returnsModule); 38 | 39 | // java.lang.Module.isNamed() 40 | MethodType returnsBoolean = MethodType.methodType(boolean.class); 41 | moduleIsNamed = MethodHandles.publicLookup().findVirtual(moduleClass, "isNamed", returnsBoolean ); 42 | 43 | // java.lang.Module.addReads(Module) 44 | MethodType addReadsSignature = MethodType.methodType(moduleClass, moduleClass); 45 | moduleAddReads = MethodHandles.lookup().findVirtual(moduleClass, "addReads", addReadsSignature) 46 | .bindTo(classGetModule.invoke(ParameterRegistration.class)); 47 | 48 | // java.lang.invoke.MethodHandles.privateLookupIn(Class, Lookup) 49 | MethodType privateLookupInSignature = MethodType.methodType(Lookup.class, Class.class, Lookup.class); 50 | privateLookupIn = MethodHandles.lookup().findStatic(MethodHandles.class, "privateLookupIn", privateLookupInSignature); 51 | } catch (RuntimeException e) { 52 | throw e; 53 | } catch (ReflectiveOperationException e) { 54 | throw new RuntimeException("could not initialize class", e); 55 | } catch (Error e) { 56 | throw e; 57 | } catch (Throwable e) { 58 | throw new RuntimeException("could not initialize class", e); 59 | } 60 | CLASS_GETMODULE = classGetModule; 61 | MODULE_ISNAMED = moduleIsNamed; 62 | MODULE_ADDREADS = moduleAddReads; 63 | PRIVE_LOOKUP_IN = privateLookupIn; 64 | } 65 | 66 | private final Class interfaceDeclaration; 67 | 68 | /** 69 | * We assume this is uncontended since we only do a few lookups and gets. 70 | * Save the memory overhead of a {@link ConcurrentHashMap}. 71 | */ 72 | private final Map defaultMethodCache; 73 | 74 | private final ReadWriteLock cacheLock; 75 | 76 | Java9DefaultMethodSupport(Class interfaceDeclaration) { 77 | this.interfaceDeclaration = interfaceDeclaration; 78 | this.defaultMethodCache = new HashMap<>(); 79 | this.cacheLock = new ReentrantReadWriteLock(); 80 | } 81 | 82 | @Override 83 | public Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable { 84 | return getDefaultMethodHandle(proxy, method).invokeWithArguments(args); 85 | } 86 | 87 | private MethodHandle getDefaultMethodHandle(Object proxy, Method method) { 88 | MethodHandle methodHandle = this.getDefaultMethodHandleFromCacheOrNull(method); 89 | if (methodHandle != null) { 90 | return methodHandle; 91 | } 92 | 93 | // potentially compute callInfo multiple times 94 | // rather than locking for a long time 95 | methodHandle = this.lookupDefaultMethod(proxy, method); 96 | 97 | MethodHandle previous = this.tryWriteDefaultMethodHandleToCache(method, methodHandle); 98 | return previous != null ? previous : methodHandle; 99 | } 100 | 101 | private MethodHandle getDefaultMethodHandleFromCacheOrNull(Method method) { 102 | Lock lock = this.cacheLock.readLock(); 103 | lock.lock(); 104 | try { 105 | return this.defaultMethodCache.get(method); 106 | } finally { 107 | lock.unlock(); 108 | } 109 | } 110 | 111 | private MethodHandle tryWriteDefaultMethodHandleToCache(Method method, MethodHandle methodHandle) { 112 | Lock lock = this.cacheLock.writeLock(); 113 | lock.lock(); 114 | try { 115 | return this.defaultMethodCache.putIfAbsent(method, methodHandle); 116 | } finally { 117 | lock.unlock(); 118 | } 119 | } 120 | 121 | private MethodHandle lookupDefaultMethod(Object proxy, Method method) { 122 | // https://gist.github.com/raphw/c1faf2f40e80afce6f13511098cfb90f 123 | try { 124 | Lookup lookup; 125 | try { 126 | // proxy.getClass().getModule().isNamed() 127 | if ((boolean) MODULE_ISNAMED.invoke(CLASS_GETMODULE.invoke(proxy.getClass()))) { 128 | lookup = (Lookup) PRIVE_LOOKUP_IN.invoke(this.interfaceDeclaration, MethodHandles.lookup()); 129 | } else { 130 | // ProcedureCaller.class.getModule().addReads(proxy.getClass().getModule()); 131 | MODULE_ADDREADS.invoke(CLASS_GETMODULE.invoke(proxy.getClass())); 132 | lookup = (Lookup) PRIVE_LOOKUP_IN.invoke(proxy.getClass(), MethodHandles.lookup()); 133 | } 134 | } catch (RuntimeException e) { 135 | throw e; 136 | } catch (Error e) { 137 | throw e; 138 | } catch (Throwable e) { 139 | throw new RuntimeException("create lookup for default method", e); 140 | } 141 | MethodType methodType = MethodType.methodType(method.getReturnType(), method.getParameterTypes()); 142 | return lookup.findSpecial(this.interfaceDeclaration, method.getName(), methodType, this.interfaceDeclaration) 143 | .bindTo(proxy); 144 | } catch (ReflectiveOperationException e) { 145 | throw new IllegalArgumentException("default method " + method + " is not accessible", e); 146 | } 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /src/test/java/com/github/marschall/storedprocedureproxy/PostgresTest.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertArrayEquals; 4 | import static org.junit.jupiter.api.Assertions.assertEquals; 5 | import static org.junit.jupiter.api.Assertions.assertThrows; 6 | import static org.junit.jupiter.api.Assertions.assertTrue; 7 | 8 | import java.sql.SQLException; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | import org.postgresql.util.PSQLException; 13 | import org.springframework.dao.DataAccessException; 14 | import org.springframework.test.context.ContextConfiguration; 15 | import org.springframework.test.context.jdbc.Sql; 16 | import org.springframework.test.context.jdbc.SqlConfig; 17 | import org.springframework.transaction.annotation.Transactional; 18 | 19 | import com.github.marschall.storedprocedureproxy.ProcedureCallerFactory.ParameterRegistration; 20 | import com.github.marschall.storedprocedureproxy.configuration.PostgresConfiguration; 21 | import com.github.marschall.storedprocedureproxy.procedures.PostgresProcedures; 22 | 23 | @Transactional 24 | @ContextConfiguration(classes = PostgresConfiguration.class) 25 | @Sql(scripts = "classpath:sql/postgres_procedures.sql", config = @SqlConfig(separator = "@")) 26 | public class PostgresTest extends AbstractDataSourceTest { 27 | 28 | private PostgresProcedures procedures(ParameterRegistration parameterRegistration) { 29 | return ProcedureCallerFactory.of(PostgresProcedures.class, this.getDataSource()) 30 | .withParameterRegistration(parameterRegistration) 31 | .withPostgresArrays() 32 | .build(); 33 | } 34 | 35 | @IndexedParametersRegistrationTest 36 | public void browserVersion(ParameterRegistration parameterRegistration) { 37 | assertEquals("Servo/0.0.1", this.procedures(parameterRegistration).browserVersion("Servo", "0.0.1")); 38 | } 39 | 40 | @IndexedParametersRegistrationTest 41 | public void salesTax(ParameterRegistration parameterRegistration) { 42 | assertEquals(0.01f, 6.0f, this.procedures(parameterRegistration).salesTax(100.0f)); 43 | } 44 | 45 | @IndexedParametersRegistrationTest 46 | public void propertyTax(ParameterRegistration parameterRegistration) { 47 | assertEquals(0.01f, 6.0f, this.procedures(parameterRegistration).propertyTax(100.0f)); 48 | } 49 | 50 | @IndexedParametersRegistrationTest 51 | public void raiseCheckedException(ParameterRegistration parameterRegistration) { 52 | SQLException sqlException = assertThrows(SQLException.class, () -> this.procedures(parameterRegistration).raiseCheckedException()); 53 | assertTrue(sqlException instanceof PSQLException); 54 | assertEquals("22000", sqlException.getSQLState()); 55 | } 56 | 57 | @IndexedParametersRegistrationTest 58 | public void raiseUncheckedException(ParameterRegistration parameterRegistration) { 59 | DataAccessException e = assertThrows(DataAccessException.class, () -> this.procedures(parameterRegistration).raiseUncheckedException()); 60 | Throwable cause = e.getCause(); 61 | assertTrue(cause instanceof PSQLException); 62 | assertEquals("22000", ((SQLException) cause).getSQLState()); 63 | } 64 | 65 | @IndexedParametersRegistrationTest 66 | public void simpleRefCursor(ParameterRegistration parameterRegistration) { 67 | List refCursor = this.procedures(parameterRegistration).simpleRefCursor(); 68 | assertEquals(Arrays.asList("hello", "postgres"), refCursor); 69 | } 70 | 71 | @IndexedParametersRegistrationTest 72 | public void simpleRefCursorOut(ParameterRegistration parameterRegistration) { 73 | List refCursor = this.procedures(parameterRegistration).simpleRefCursorOut(); 74 | assertEquals(Arrays.asList("hello", "postgres"), refCursor); 75 | } 76 | 77 | @IndexedParametersRegistrationTest 78 | public void mappedRefCursorNumbered(ParameterRegistration parameterRegistration) { 79 | List refCursor = this.procedures(parameterRegistration).mappedRefCursor((rs, i) -> i + "-" + rs.getString(1)); 80 | assertEquals(Arrays.asList("0-hello", "1-postgres"), refCursor); 81 | } 82 | 83 | @IndexedParametersRegistrationTest 84 | public void mappedRefCursor(ParameterRegistration parameterRegistration) { 85 | List refCursor = this.procedures(parameterRegistration).mappedRefCursor(rs -> "1-" + rs.getString(1)); 86 | assertEquals(Arrays.asList("1-hello", "1-postgres"), refCursor); 87 | } 88 | 89 | @IndexedParametersRegistrationTest 90 | public void mappedRefCursorAndArgumentNumbered(ParameterRegistration parameterRegistration) { 91 | List refCursor = this.procedures(parameterRegistration).mappedRefCursorAndArgument("prefix-", (rs, i) -> i + "-" + rs.getString(1)); 92 | assertEquals(Arrays.asList("0-prefix-hello", "1-prefix-postgres"), refCursor); 93 | } 94 | 95 | @IndexedParametersRegistrationTest 96 | public void mappedRefCursorAndArgument(ParameterRegistration parameterRegistration) { 97 | List refCursor = this.procedures(parameterRegistration).mappedRefCursorAndArgument("prefix-", rs -> "1-" + rs.getString(1)); 98 | assertEquals(Arrays.asList("1-prefix-hello", "1-prefix-postgres"), refCursor); 99 | } 100 | 101 | @IndexedParametersRegistrationTest 102 | public void sampleArrayArgumentList(ParameterRegistration parameterRegistration) { 103 | String result = this.procedures(parameterRegistration).sampleArrayArgumentList(Arrays.asList(1, 2, 3)); 104 | assertEquals("1, 2, 3", result); 105 | } 106 | 107 | @IndexedParametersRegistrationTest 108 | public void sampleArrayArgumentArray(ParameterRegistration parameterRegistration) { 109 | String result = this.procedures(parameterRegistration).sampleArrayArgumentArray(new Integer[] {1, 2, 3}); 110 | assertEquals("1, 2, 3", result); 111 | } 112 | 113 | @IndexedParametersRegistrationTest 114 | public void sampleArrayArgumentPrimitiveArray(ParameterRegistration parameterRegistration) { 115 | String result = this.procedures(parameterRegistration).sampleArrayArgumentPrimitiveArray(new int[] {1, 2, 3}); 116 | assertEquals("1, 2, 3", result); 117 | } 118 | 119 | @IndexedParametersRegistrationTest 120 | public void concatenateTwoArrays(ParameterRegistration parameterRegistration) { 121 | Integer[] result = this.procedures(parameterRegistration).concatenateTwoArrays(new Integer[] {1, 2, 3}, new Integer[] {4, 5, 6}); 122 | assertArrayEquals(new Integer[] {1, 2, 3, 4, 5, 6}, result); 123 | } 124 | 125 | @IndexedParametersRegistrationTest 126 | public void arrayReturnValuePrimitive(ParameterRegistration parameterRegistration) { 127 | int[] result = this.procedures(parameterRegistration).arrayReturnValuePrimitive(); 128 | assertArrayEquals(new int[] {1, 2, 3, 4}, result); 129 | } 130 | 131 | @IndexedParametersRegistrationTest 132 | public void arrayReturnValueRefernce(ParameterRegistration parameterRegistration) { 133 | Integer[] result = this.procedures(parameterRegistration).arrayReturnValueRefernce(); 134 | assertArrayEquals(new Integer[] {1, 2, 3, 4}, result); 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/com/github/marschall/storedprocedureproxy/spi/NamingStrategy.java: -------------------------------------------------------------------------------- 1 | package com.github.marschall.storedprocedureproxy.spi; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * Derives a database name of an object from the Java name of an object. 7 | * 8 | *

Provides various convenience methods for chaining several 9 | * implementations. For example if the Java name of your stored procedure 10 | * is {@code "blitz"} but the SQL name is {@code "sp_Blitz"} then you can 11 | * create this transformation using:

12 | *

 13 |  * NamingStrategy.capitalize() // converts "blitz" to "Blitz"
 14 |  *    .thenPrefix("sp_") // converts "Blitz" to "sp_Blitz"
 15 |  * 
16 | * 17 | * @see Deriving Names 18 | */ 19 | @FunctionalInterface 20 | public interface NamingStrategy { 21 | 22 | /** 23 | * The identity transformation. Simply returns the argument unchanged. 24 | */ 25 | public static NamingStrategy IDENTITY = (s) -> s; 26 | 27 | /** 28 | * Derives a database name of an object from the Java name of an object. 29 | * 30 | * @param javaName the Java name of an object, never {@code null} 31 | * @return the database name of an object, never {@code null} 32 | */ 33 | String translateToDatabase(String javaName); 34 | 35 | /** 36 | * Creates a new transformation that converts the entire string to upper case. 37 | * 38 | *

Only works reliably for characters from the US-ASCII latin alphabet.

39 | * 40 | * @return a new transformation that converts the entire string to upper case 41 | */ 42 | public static NamingStrategy upperCase() { 43 | return UpperCase.INSTANCE; 44 | } 45 | 46 | /** 47 | * Creates a new transformation that converts the entire string to lower case. 48 | * 49 | *

Only works reliably for characters from the US-ASCII latin alphabet.

50 | * 51 | * @return a new transformation that converts the entire string to lower case 52 | */ 53 | public static NamingStrategy lowerCase() { 54 | return LowerCase.INSTANCE; 55 | } 56 | 57 | /** 58 | * Creates a new transformation that converts the first character to 59 | * upper case. 60 | * 61 | *

Only works for characters from the US-ASCII latin alphabet.

62 | * 63 | * @return a new transformation that converts the first character to upper case 64 | */ 65 | public static NamingStrategy capitalize() { 66 | return Capitalize.INSTANCE; 67 | } 68 | 69 | /** 70 | * Creates a new transformation that converts the entire string to 71 | * snake case. 72 | * 73 | *

For example turns {@code "procedureName"} into {@code "procedure_Name"}. 74 | * No case conversion is done so you'll likely want to combine this with 75 | * either {@link #thenUpperCase()} or {@link #thenLowerCase()}.

76 | * 77 | * @return a new transformation that converts the entire string to snake case 78 | */ 79 | public static NamingStrategy snakeCase() { 80 | return SnakeCase.INSTANCE; 81 | } 82 | 83 | /** 84 | * Creates a new transformation that applies a prefix to the string. 85 | * 86 | * @param prefix the prefix to append, not {@code null} 87 | * @return a new transformation that applies a prefix 88 | */ 89 | public static NamingStrategy prefix(String prefix) { 90 | Objects.requireNonNull(prefix); 91 | return new Prefix(prefix); 92 | } 93 | 94 | /** 95 | * Creates a new transformation that skips a given number of characters 96 | * from the start of the java name. 97 | * 98 | * @param skipped the number of characters from the start 99 | * @return a new transformation that skips the first characters 100 | */ 101 | public static NamingStrategy withoutFirst(int skipped) { 102 | return new WithoutFirst(skipped); 103 | } 104 | 105 | /** 106 | * Applies another transformation after the current transformation. 107 | * 108 | * @param next the transformation to apply after the current one, not {@code null} 109 | * @return a new transformation that applies the given transformation after the current transformation 110 | */ 111 | default NamingStrategy then(NamingStrategy next) { 112 | Objects.requireNonNull(next); 113 | return new Compund(this, next); 114 | } 115 | 116 | /** 117 | * Applies a upper case transformation of the entire string after the current transformation. 118 | * 119 | *

Only works reliably for characters from the US-ASCII latin alphabet.

120 | * 121 | * @return a new transformation that applies a upper case transformation after the current transformation 122 | */ 123 | default NamingStrategy thenUpperCase() { 124 | return then(upperCase()); 125 | } 126 | 127 | /** 128 | * Applies a lower case transformation of the entire string after the current transformation. 129 | * 130 | *

Only works reliably for characters from the US-ASCII latin alphabet.

131 | * 132 | * @return a new transformation that applies a lower case transformation after the current transformation 133 | */ 134 | default NamingStrategy thenLowerCase() { 135 | return then(lowerCase()); 136 | } 137 | 138 | /** 139 | * Applies an upper case transformation of the first character after the 140 | * current transformation. 141 | * 142 | *

Only works for characters from the US-ASCII latin alphabet.

143 | * 144 | * @return a new transformation that applies captialisation of the first 145 | * character after the current transformation 146 | */ 147 | default NamingStrategy thenCapitalize() { 148 | return then(capitalize()); 149 | } 150 | 151 | /** 152 | * Applies snake case 153 | * after the current transformation. 154 | * 155 | *

For example turns {@code "procedureName"} into {@code "procedure_Name"}. 156 | * No case conversion is done so you'll likely want to combine this with 157 | * either {@link #thenUpperCase()} or {@link #thenLowerCase()}.

158 | * 159 | * @return a new transformation that applies snake case after the current transformation 160 | */ 161 | default NamingStrategy thenSnakeCase() { 162 | return then(snakeCase()); 163 | } 164 | 165 | /** 166 | * Appends a prefix after the current transformation. 167 | * 168 | * @param prefix the prefix to append, not {@code null} 169 | * @return a new transformation that applies a prefix after the current transformation 170 | */ 171 | default NamingStrategy thenPrefix(String prefix) { 172 | Objects.requireNonNull(prefix); 173 | return then(prefix(prefix)); 174 | } 175 | 176 | /** 177 | * Skips a number of characters from the start of the name after the current 178 | * transformation. 179 | * 180 | * @param skipped the number of characters from the start 181 | * @return a new transformation that skips {@code skipped} characters after the current transformation 182 | */ 183 | default NamingStrategy thenWithoutFirst(int skipped) { 184 | return then(withoutFirst(skipped)); 185 | } 186 | 187 | } 188 | --------------------------------------------------------------------------------