001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.jdbc.core.metadata;
018
019import java.sql.DatabaseMetaData;
020import java.sql.ResultSet;
021import java.sql.SQLException;
022import java.sql.Types;
023import java.util.ArrayList;
024import java.util.List;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028
029import org.springframework.dao.InvalidDataAccessApiUsageException;
030import org.springframework.jdbc.core.SqlInOutParameter;
031import org.springframework.jdbc.core.SqlOutParameter;
032import org.springframework.jdbc.core.SqlParameter;
033import org.springframework.util.StringUtils;
034
035/**
036 * A generic implementation of the {@link CallMetaDataProvider} interface.
037 * This class can be extended to provide database specific behavior.
038 *
039 * @author Thomas Risberg
040 * @author Juergen Hoeller
041 * @since 2.5
042 */
043public class GenericCallMetaDataProvider implements CallMetaDataProvider {
044
045        /** Logger available to subclasses */
046        protected static final Log logger = LogFactory.getLog(CallMetaDataProvider.class);
047
048        private boolean procedureColumnMetaDataUsed = false;
049
050        private String userName;
051
052        private boolean supportsCatalogsInProcedureCalls = true;
053
054        private boolean supportsSchemasInProcedureCalls = true;
055
056        private boolean storesUpperCaseIdentifiers = true;
057
058        private boolean storesLowerCaseIdentifiers = false;
059
060        private List<CallParameterMetaData> callParameterMetaData = new ArrayList<CallParameterMetaData>();
061
062
063        /**
064         * Constructor used to initialize with provided database meta-data.
065         * @param databaseMetaData meta-data to be used
066         */
067        protected GenericCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException {
068                this.userName = databaseMetaData.getUserName();
069        }
070
071
072        @Override
073        public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException {
074                try {
075                        setSupportsCatalogsInProcedureCalls(databaseMetaData.supportsCatalogsInProcedureCalls());
076                }
077                catch (SQLException ex) {
078                        if (logger.isWarnEnabled()) {
079                                logger.warn("Error retrieving 'DatabaseMetaData.supportsCatalogsInProcedureCalls': " + ex.getMessage());
080                        }
081                }
082                try {
083                        setSupportsSchemasInProcedureCalls(databaseMetaData.supportsSchemasInProcedureCalls());
084                }
085                catch (SQLException ex) {
086                        if (logger.isWarnEnabled()) {
087                                logger.warn("Error retrieving 'DatabaseMetaData.supportsSchemasInProcedureCalls': " + ex.getMessage());
088                        }
089                }
090                try {
091                        setStoresUpperCaseIdentifiers(databaseMetaData.storesUpperCaseIdentifiers());
092                }
093                catch (SQLException ex) {
094                        if (logger.isWarnEnabled()) {
095                                logger.warn("Error retrieving 'DatabaseMetaData.storesUpperCaseIdentifiers': " + ex.getMessage());
096                        }
097                }
098                try {
099                        setStoresLowerCaseIdentifiers(databaseMetaData.storesLowerCaseIdentifiers());
100                }
101                catch (SQLException ex) {
102                        if (logger.isWarnEnabled()) {
103                                logger.warn("Error retrieving 'DatabaseMetaData.storesLowerCaseIdentifiers': " + ex.getMessage());
104                        }
105                }
106        }
107
108        @Override
109        public void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, String catalogName,
110                        String schemaName, String procedureName) throws SQLException {
111
112                this.procedureColumnMetaDataUsed = true;
113                processProcedureColumns(databaseMetaData, catalogName, schemaName,  procedureName);
114        }
115
116        @Override
117        public List<CallParameterMetaData> getCallParameterMetaData() {
118                return this.callParameterMetaData;
119        }
120
121        @Override
122        public String procedureNameToUse(String procedureName) {
123                if (procedureName == null) {
124                        return null;
125                }
126                else if (isStoresUpperCaseIdentifiers()) {
127                        return procedureName.toUpperCase();
128                }
129                else if (isStoresLowerCaseIdentifiers()) {
130                        return procedureName.toLowerCase();
131                }
132                else {
133                        return procedureName;
134                }
135        }
136
137        @Override
138        public String catalogNameToUse(String catalogName) {
139                if (catalogName == null) {
140                        return null;
141                }
142                else if (isStoresUpperCaseIdentifiers()) {
143                        return catalogName.toUpperCase();
144                }
145                else if (isStoresLowerCaseIdentifiers()) {
146                        return catalogName.toLowerCase();
147                }
148                else {
149                        return catalogName;
150                }
151        }
152
153        @Override
154        public String schemaNameToUse(String schemaName) {
155                if (schemaName == null) {
156                        return null;
157                }
158                else if (isStoresUpperCaseIdentifiers()) {
159                        return schemaName.toUpperCase();
160                }
161                else if (isStoresLowerCaseIdentifiers()) {
162                        return schemaName.toLowerCase();
163                }
164                else {
165                        return schemaName;
166                }
167        }
168
169        @Override
170        public String metaDataCatalogNameToUse(String catalogName) {
171                if (isSupportsCatalogsInProcedureCalls()) {
172                        return catalogNameToUse(catalogName);
173                }
174                else {
175                        return null;
176                }
177        }
178
179        @Override
180        public String metaDataSchemaNameToUse(String schemaName) {
181                if (isSupportsSchemasInProcedureCalls()) {
182                        return schemaNameToUse(schemaName);
183                }
184                else {
185                        return null;
186                }
187        }
188
189        @Override
190        public String parameterNameToUse(String parameterName) {
191                if (parameterName == null) {
192                        return null;
193                }
194                else if (isStoresUpperCaseIdentifiers()) {
195                        return parameterName.toUpperCase();
196                }
197                else if (isStoresLowerCaseIdentifiers()) {
198                        return parameterName.toLowerCase();
199                }
200                else {
201                        return parameterName;
202                }
203        }
204
205        @Override
206        public boolean byPassReturnParameter(String parameterName) {
207                return false;
208        }
209
210        @Override
211        public SqlParameter createDefaultOutParameter(String parameterName, CallParameterMetaData meta) {
212                return new SqlOutParameter(parameterName, meta.getSqlType());
213        }
214
215        @Override
216        public SqlParameter createDefaultInOutParameter(String parameterName, CallParameterMetaData meta) {
217                return new SqlInOutParameter(parameterName, meta.getSqlType());
218        }
219
220        @Override
221        public SqlParameter createDefaultInParameter(String parameterName, CallParameterMetaData meta) {
222                return new SqlParameter(parameterName, meta.getSqlType());
223        }
224
225        @Override
226        public String getUserName() {
227                return this.userName;
228        }
229
230        @Override
231        public boolean isReturnResultSetSupported() {
232                return true;
233        }
234
235        @Override
236        public boolean isRefCursorSupported() {
237                return false;
238        }
239
240        @Override
241        public int getRefCursorSqlType() {
242                return Types.OTHER;
243        }
244
245        @Override
246        public boolean isProcedureColumnMetaDataUsed() {
247                return this.procedureColumnMetaDataUsed;
248        }
249
250
251        /**
252         * Specify whether the database supports the use of catalog name in procedure calls.
253         */
254        protected void setSupportsCatalogsInProcedureCalls(boolean supportsCatalogsInProcedureCalls) {
255                this.supportsCatalogsInProcedureCalls = supportsCatalogsInProcedureCalls;
256        }
257
258        /**
259         * Does the database support the use of catalog name in procedure calls?
260         */
261        @Override
262        public boolean isSupportsCatalogsInProcedureCalls() {
263                return this.supportsCatalogsInProcedureCalls;
264        }
265
266        /**
267         * Specify whether the database supports the use of schema name in procedure calls.
268         */
269        protected void setSupportsSchemasInProcedureCalls(boolean supportsSchemasInProcedureCalls) {
270                this.supportsSchemasInProcedureCalls = supportsSchemasInProcedureCalls;
271        }
272
273        /**
274         * Does the database support the use of schema name in procedure calls?
275         */
276        @Override
277        public boolean isSupportsSchemasInProcedureCalls() {
278                return this.supportsSchemasInProcedureCalls;
279        }
280
281        /**
282         * Specify whether the database uses upper case for identifiers.
283         */
284        protected void setStoresUpperCaseIdentifiers(boolean storesUpperCaseIdentifiers) {
285                this.storesUpperCaseIdentifiers = storesUpperCaseIdentifiers;
286        }
287
288        /**
289         * Does the database use upper case for identifiers?
290         */
291        protected boolean isStoresUpperCaseIdentifiers() {
292                return this.storesUpperCaseIdentifiers;
293        }
294
295        /**
296         * Specify whether the database uses lower case for identifiers.
297         */
298        protected void setStoresLowerCaseIdentifiers(boolean storesLowerCaseIdentifiers) {
299                this.storesLowerCaseIdentifiers = storesLowerCaseIdentifiers;
300        }
301
302        /**
303         * Does the database use lower case for identifiers?
304         */
305        protected boolean isStoresLowerCaseIdentifiers() {
306                return this.storesLowerCaseIdentifiers;
307        }
308
309
310        /**
311         * Process the procedure column meta-data.
312         */
313        private void processProcedureColumns(
314                        DatabaseMetaData databaseMetaData, String catalogName, String schemaName, String procedureName) {
315
316                String metaDataCatalogName = metaDataCatalogNameToUse(catalogName);
317                String metaDataSchemaName = metaDataSchemaNameToUse(schemaName);
318                String metaDataProcedureName = procedureNameToUse(procedureName);
319                if (logger.isDebugEnabled()) {
320                        logger.debug("Retrieving meta-data for " + metaDataCatalogName + '/' +
321                                        metaDataSchemaName + '/' + metaDataProcedureName);
322                }
323
324                ResultSet procs = null;
325                try {
326                        procs = databaseMetaData.getProcedures(metaDataCatalogName, metaDataSchemaName, metaDataProcedureName);
327                        List<String> found = new ArrayList<String>();
328                        while (procs.next()) {
329                                found.add(procs.getString("PROCEDURE_CAT") + '.' + procs.getString("PROCEDURE_SCHEM") +
330                                                '.' + procs.getString("PROCEDURE_NAME"));
331                        }
332                        procs.close();
333
334                        if (found.size() > 1) {
335                                throw new InvalidDataAccessApiUsageException(
336                                                "Unable to determine the correct call signature - multiple " +
337                                                "procedures/functions/signatures for '" + metaDataProcedureName + "': found " + found);
338                        }
339                        else if (found.isEmpty()) {
340                                if (metaDataProcedureName.contains(".") && !StringUtils.hasText(metaDataCatalogName)) {
341                                        String packageName = metaDataProcedureName.substring(0, metaDataProcedureName.indexOf('.'));
342                                        throw new InvalidDataAccessApiUsageException(
343                                                        "Unable to determine the correct call signature for '" + metaDataProcedureName +
344                                                        "' - package name should be specified separately using '.withCatalogName(\"" +
345                                                        packageName + "\")'");
346                                }
347                                else if ("Oracle".equals(databaseMetaData.getDatabaseProductName())) {
348                                        if (logger.isDebugEnabled()) {
349                                                logger.debug("Oracle JDBC driver did not return procedure/function/signature for '" +
350                                                                metaDataProcedureName + "' - assuming a non-exposed synonym");
351                                        }
352                                }
353                                else {
354                                        throw new InvalidDataAccessApiUsageException(
355                                                        "Unable to determine the correct call signature - no " +
356                                                        "procedure/function/signature for '" + metaDataProcedureName + "'");
357                                }
358                        }
359
360                        procs = databaseMetaData.getProcedureColumns(
361                                        metaDataCatalogName, metaDataSchemaName, metaDataProcedureName, null);
362                        while (procs.next()) {
363                                String columnName = procs.getString("COLUMN_NAME");
364                                int columnType = procs.getInt("COLUMN_TYPE");
365                                if (columnName == null && (
366                                                columnType == DatabaseMetaData.procedureColumnIn  ||
367                                                columnType == DatabaseMetaData.procedureColumnInOut ||
368                                                columnType == DatabaseMetaData.procedureColumnOut)) {
369                                        if (logger.isDebugEnabled()) {
370                                                logger.debug("Skipping meta-data for: " + columnType + " " + procs.getInt("DATA_TYPE") +
371                                                        " " + procs.getString("TYPE_NAME") + " " + procs.getInt("NULLABLE") +
372                                                        " (probably a member of a collection)");
373                                        }
374                                }
375                                else {
376                                        CallParameterMetaData meta = new CallParameterMetaData(columnName, columnType,
377                                                        procs.getInt("DATA_TYPE"), procs.getString("TYPE_NAME"),
378                                                        procs.getInt("NULLABLE") == DatabaseMetaData.procedureNullable);
379                                        this.callParameterMetaData.add(meta);
380                                        if (logger.isDebugEnabled()) {
381                                                logger.debug("Retrieved meta-data: " + meta.getParameterName() + " " +
382                                                                meta.getParameterType() + " " + meta.getSqlType() + " " +
383                                                                meta.getTypeName() + " " + meta.isNullable());
384                                        }
385                                }
386                        }
387                }
388                catch (SQLException ex) {
389                        if (logger.isWarnEnabled()) {
390                                logger.warn("Error while retrieving meta-data for procedure columns: " + ex);
391                        }
392                }
393                finally {
394                        try {
395                                if (procs != null) {
396                                        procs.close();
397                                }
398                        }
399                        catch (SQLException ex) {
400                                if (logger.isWarnEnabled()) {
401                                        logger.warn("Problem closing ResultSet for procedure column meta-data: " + ex);
402                                }
403                        }
404                }
405        }
406
407}