diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java index 3822fae85baa..1ae9ff0e67d3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java @@ -22,6 +22,7 @@ import java.sql.Types; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -41,6 +42,7 @@ * @author Thomas Risberg * @author Juergen Hoeller * @author Sam Brannen + * @author Stephane Nicoll * @since 2.5 */ public class GenericCallMetaDataProvider implements CallMetaDataProvider { @@ -298,17 +300,24 @@ private void processProcedureColumns(DatabaseMetaData databaseMetaData, String metaDataCatalogName = metaDataCatalogNameToUse(catalogName); String metaDataSchemaName = metaDataSchemaNameToUse(schemaName); String metaDataProcedureName = procedureNameToUse(procedureName); - if (logger.isDebugEnabled()) { - logger.debug("Retrieving meta-data for " + metaDataCatalogName + '/' + - metaDataSchemaName + '/' + metaDataProcedureName); - } - try { + String searchStringEscape = databaseMetaData.getSearchStringEscape(); + String escapedSchemaName = escapeNamePattern(metaDataSchemaName, searchStringEscape); + String escapedProcedureName = escapeNamePattern(metaDataProcedureName, searchStringEscape); + if (logger.isDebugEnabled()) { + String schemaInfo = (Objects.equals(escapedSchemaName, metaDataSchemaName) + ? metaDataSchemaName : metaDataCatalogName + "(" + escapedSchemaName + ")"); + String procedureInfo = (Objects.equals(escapedProcedureName, metaDataProcedureName) + ? metaDataProcedureName : metaDataProcedureName + "(" + escapedProcedureName + ")"); + logger.debug("Retrieving meta-data for " + metaDataCatalogName + '/' + + schemaInfo + '/' + procedureInfo); + } + List found = new ArrayList<>(); boolean function = false; try (ResultSet procedures = databaseMetaData.getProcedures( - metaDataCatalogName, metaDataSchemaName, metaDataProcedureName)) { + metaDataCatalogName, escapedSchemaName, escapedProcedureName)) { while (procedures.next()) { found.add(procedures.getString("PROCEDURE_CAT") + '.' + procedures.getString("PROCEDURE_SCHEM") + '.' + procedures.getString("PROCEDURE_NAME")); @@ -318,7 +327,7 @@ private void processProcedureColumns(DatabaseMetaData databaseMetaData, if (found.isEmpty()) { // Functions not exposed as procedures anymore on PostgreSQL driver 42.2.11 try (ResultSet functions = databaseMetaData.getFunctions( - metaDataCatalogName, metaDataSchemaName, metaDataProcedureName)) { + metaDataCatalogName, escapedSchemaName, escapedProcedureName)) { while (functions.next()) { found.add(functions.getString("FUNCTION_CAT") + '.' + functions.getString("FUNCTION_SCHEM") + '.' + functions.getString("FUNCTION_NAME")); @@ -359,8 +368,8 @@ else if ("Oracle".equals(databaseMetaData.getDatabaseProductName())) { metaDataCatalogName + '/' + metaDataSchemaName + '/' + metaDataProcedureName); } try (ResultSet columns = function ? - databaseMetaData.getFunctionColumns(metaDataCatalogName, metaDataSchemaName, metaDataProcedureName, null) : - databaseMetaData.getProcedureColumns(metaDataCatalogName, metaDataSchemaName, metaDataProcedureName, null)) { + databaseMetaData.getFunctionColumns(metaDataCatalogName, escapedSchemaName, escapedProcedureName, null) : + databaseMetaData.getProcedureColumns(metaDataCatalogName, escapedSchemaName, escapedProcedureName, null)) { while (columns.next()) { String columnName = columns.getString("COLUMN_NAME"); int columnType = columns.getInt("COLUMN_TYPE"); @@ -400,6 +409,16 @@ else if ("Oracle".equals(databaseMetaData.getDatabaseProductName())) { } } + @Nullable + private static String escapeNamePattern(@Nullable String name, @Nullable String escape) { + if (name == null || escape == null) { + return name; + } + return name.replace(escape, escape + escape) + .replace("_", escape + "_") + .replace("%", escape + "%"); + } + private static boolean isInOrOutColumn(int columnType, boolean function) { if (function) { return (columnType == DatabaseMetaData.functionColumnIn || diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java new file mode 100644 index 000000000000..7aaf9e5271d7 --- /dev/null +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.jdbc.core.metadata; + +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link GenericCallMetaDataProvider}. + * + * @author Stephane Nicoll + */ +class GenericCallMetaDataProviderTests { + + private final DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class); + + @Test + void procedureNameWithPatternIsEscape() throws SQLException { + given(this.databaseMetaData.getSearchStringEscape()).willReturn("@"); + GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider(this.databaseMetaData); + given(this.databaseMetaData.getProcedures(null, null, "MY@_PROCEDURE")) + .willThrow(new IllegalStateException("Expected")); + assertThatIllegalStateException().isThrownBy(() -> provider.initializeWithProcedureColumnMetaData( + this.databaseMetaData, null, null, "my_procedure")); + verify(this.databaseMetaData).getProcedures(null, null, "MY@_PROCEDURE"); + } + + @Test + void schemaNameWithPatternIsEscape() throws SQLException { + given(this.databaseMetaData.getSearchStringEscape()).willReturn("@"); + GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider(this.databaseMetaData); + given(this.databaseMetaData.getProcedures(null, "MY@_SCHEMA", "TEST")) + .willThrow(new IllegalStateException("Expected")); + assertThatIllegalStateException().isThrownBy(() -> provider.initializeWithProcedureColumnMetaData( + this.databaseMetaData, null, "my_schema", "test")); + verify(this.databaseMetaData).getProcedures(null, "MY@_SCHEMA", "TEST"); + } + + @Test + void nameIsNotEscapedIfEscapeCharacterIsNotAvailable() throws SQLException { + given(this.databaseMetaData.getSearchStringEscape()).willReturn(null); + GenericCallMetaDataProvider provider = new GenericCallMetaDataProvider(this.databaseMetaData); + given(this.databaseMetaData.getProcedures(null, "MY_SCHEMA", "MY_TEST")) + .willThrow(new IllegalStateException("Expected")); + assertThatIllegalStateException().isThrownBy(() -> provider.initializeWithProcedureColumnMetaData( + this.databaseMetaData, null, "my_schema", "my_test")); + verify(this.databaseMetaData).getProcedures(null, "MY_SCHEMA", "MY_TEST"); + } + +} diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java index 7cb129c42091..b99cd1591da2 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java @@ -239,8 +239,9 @@ void exceptionThrownWhileRetrievingColumnNamesFromMetadata() throws Exception { given(databaseMetaData.getDatabaseProductName()).willReturn("Oracle"); given(databaseMetaData.getUserName()).willReturn("ME"); given(databaseMetaData.storesUpperCaseIdentifiers()).willReturn(true); - given(databaseMetaData.getProcedures("", "ME", "ADD_INVOICE")).willReturn(proceduresResultSet); - given(databaseMetaData.getProcedureColumns("", "ME", "ADD_INVOICE", null)).willReturn(procedureColumnsResultSet); + given(databaseMetaData.getSearchStringEscape()).willReturn("@"); + given(databaseMetaData.getProcedures("", "ME", "ADD@_INVOICE")).willReturn(proceduresResultSet); + given(databaseMetaData.getProcedureColumns("", "ME", "ADD@_INVOICE", null)).willReturn(procedureColumnsResultSet); given(proceduresResultSet.next()).willReturn(true, false); given(proceduresResultSet.getString("PROCEDURE_NAME")).willReturn("add_invoice"); @@ -306,8 +307,9 @@ private void initializeAddInvoiceWithMetaData(boolean isFunction) throws SQLExce given(databaseMetaData.getDatabaseProductName()).willReturn("Oracle"); given(databaseMetaData.getUserName()).willReturn("ME"); given(databaseMetaData.storesUpperCaseIdentifiers()).willReturn(true); - given(databaseMetaData.getProcedures("", "ME", "ADD_INVOICE")).willReturn(proceduresResultSet); - given(databaseMetaData.getProcedureColumns("", "ME", "ADD_INVOICE", null)).willReturn(procedureColumnsResultSet); + given(databaseMetaData.getSearchStringEscape()).willReturn("@"); + given(databaseMetaData.getProcedures("", "ME", "ADD@_INVOICE")).willReturn(proceduresResultSet); + given(databaseMetaData.getProcedureColumns("", "ME", "ADD@_INVOICE", null)).willReturn(procedureColumnsResultSet); given(proceduresResultSet.next()).willReturn(true, false); given(proceduresResultSet.getString("PROCEDURE_NAME")).willReturn("add_invoice"); @@ -330,8 +332,8 @@ private void initializeAddInvoiceWithMetaData(boolean isFunction) throws SQLExce } private void verifyAddInvoiceWithMetaData(boolean isFunction) throws SQLException { - ResultSet proceduresResultSet = databaseMetaData.getProcedures("", "ME", "ADD_INVOICE"); - ResultSet procedureColumnsResultSet = databaseMetaData.getProcedureColumns("", "ME", "ADD_INVOICE", null); + ResultSet proceduresResultSet = databaseMetaData.getProcedures("", "ME", "ADD@_INVOICE"); + ResultSet procedureColumnsResultSet = databaseMetaData.getProcedureColumns("", "ME", "ADD@_INVOICE", null); if (isFunction) { verify(callableStatement).registerOutParameter(1, 4); verify(callableStatement).setObject(2, 1103, 4);