View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.jdbc.core.metadata;
18  
19  import java.sql.DatabaseMetaData;
20  import java.sql.ResultSet;
21  import java.sql.SQLException;
22  import java.sql.Types;
23  import java.util.ArrayList;
24  import java.util.List;
25  
26  import org.apache.commons.logging.Log;
27  import org.apache.commons.logging.LogFactory;
28  
29  import org.springframework.dao.InvalidDataAccessApiUsageException;
30  import org.springframework.jdbc.core.SqlInOutParameter;
31  import org.springframework.jdbc.core.SqlOutParameter;
32  import org.springframework.jdbc.core.SqlParameter;
33  import org.springframework.util.StringUtils;
34  
35  /**
36   * Generic implementation for the {@link CallMetaDataProvider} interface.
37   * This class can be extended to provide database specific behavior.
38   *
39   * @author Thomas Risberg
40   * @since 2.5
41   */
42  public class GenericCallMetaDataProvider implements CallMetaDataProvider {
43  
44  	/** Logger available to subclasses */
45  	protected static final Log logger = LogFactory.getLog(CallMetaDataProvider.class);
46  
47  	private boolean procedureColumnMetaDataUsed = false;
48  
49  	private String userName;
50  
51  	private boolean supportsCatalogsInProcedureCalls = true;
52  
53  	private boolean supportsSchemasInProcedureCalls = true;
54  
55  	private boolean storesUpperCaseIdentifiers = true;
56  
57  	private boolean storesLowerCaseIdentifiers = false;
58  
59  	private List<CallParameterMetaData> callParameterMetaData = new ArrayList<CallParameterMetaData>();
60  
61  
62  	/**
63  	 * Constructor used to initialize with provided database meta data.
64  	 * @param databaseMetaData meta data to be used
65  	 */
66  	protected GenericCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLException {
67  		this.userName = databaseMetaData.getUserName();
68  	}
69  
70  
71  	@Override
72  	public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQLException {
73  		try {
74  			setSupportsCatalogsInProcedureCalls(databaseMetaData.supportsCatalogsInProcedureCalls());
75  		}
76  		catch (SQLException ex) {
77  			if (logger.isWarnEnabled()) {
78  				logger.warn("Error retrieving 'DatabaseMetaData.supportsCatalogsInProcedureCalls' - " + ex.getMessage());
79  			}
80  		}
81  		try {
82  			setSupportsSchemasInProcedureCalls(databaseMetaData.supportsSchemasInProcedureCalls());
83  		}
84  		catch (SQLException ex) {
85  			if (logger.isWarnEnabled()) {
86  				logger.warn("Error retrieving 'DatabaseMetaData.supportsSchemasInProcedureCalls' - " + ex.getMessage());
87  			}
88  		}
89  		try {
90  			setStoresUpperCaseIdentifiers(databaseMetaData.storesUpperCaseIdentifiers());
91  		}
92  		catch (SQLException ex) {
93  			if (logger.isWarnEnabled()) {
94  				logger.warn("Error retrieving 'DatabaseMetaData.storesUpperCaseIdentifiers' - " + ex.getMessage());
95  			}
96  		}
97  		try {
98  			setStoresLowerCaseIdentifiers(databaseMetaData.storesLowerCaseIdentifiers());
99  		}
100 		catch (SQLException ex) {
101 			if (logger.isWarnEnabled()) {
102 				logger.warn("Error retrieving 'DatabaseMetaData.storesLowerCaseIdentifiers' - " + ex.getMessage());
103 			}
104 		}
105 	}
106 
107 	@Override
108 	public void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, String catalogName,
109 			String schemaName, String procedureName) throws SQLException {
110 
111 		this.procedureColumnMetaDataUsed = true;
112 		processProcedureColumns(databaseMetaData, catalogName, schemaName,  procedureName);
113 	}
114 
115 	@Override
116 	public List<CallParameterMetaData> getCallParameterMetaData() {
117 		return this.callParameterMetaData;
118 	}
119 
120 	@Override
121 	public String procedureNameToUse(String procedureName) {
122 		if (procedureName == null) {
123 			return null;
124 		}
125 		else if (isStoresUpperCaseIdentifiers()) {
126 			return procedureName.toUpperCase();
127 		}
128 		else if (isStoresLowerCaseIdentifiers()) {
129 			return procedureName.toLowerCase();
130 		}
131 		else {
132 			return procedureName;
133 		}
134 	}
135 
136 	@Override
137 	public String catalogNameToUse(String catalogName) {
138 		if (catalogName == null) {
139 			return null;
140 		}
141 		else if (isStoresUpperCaseIdentifiers()) {
142 			return catalogName.toUpperCase();
143 		}
144 		else if (isStoresLowerCaseIdentifiers()) {
145 			return catalogName.toLowerCase();
146 		}
147 		else {
148 			return catalogName;
149 		}
150 	}
151 
152 	@Override
153 	public String schemaNameToUse(String schemaName) {
154 		if (schemaName == null) {
155 			return null;
156 		}
157 		else if (isStoresUpperCaseIdentifiers()) {
158 			return schemaName.toUpperCase();
159 		}
160 		else if (isStoresLowerCaseIdentifiers()) {
161 			return schemaName.toLowerCase();
162 		}
163 		else {
164 			return schemaName;
165 		}
166 	}
167 
168 	@Override
169 	public String metaDataCatalogNameToUse(String catalogName) {
170 		if (isSupportsCatalogsInProcedureCalls()) {
171 			return catalogNameToUse(catalogName);
172 		}
173 		else {
174 			return null;
175 		}
176 	}
177 
178 	@Override
179 	public String metaDataSchemaNameToUse(String schemaName) {
180 		if (isSupportsSchemasInProcedureCalls()) {
181 			return schemaNameToUse(schemaName);
182 		}
183 		else {
184 			return null;
185 		}
186 	}
187 
188 	@Override
189 	public String parameterNameToUse(String parameterName) {
190 		if (parameterName == null) {
191 			return null;
192 		}
193 		else if (isStoresUpperCaseIdentifiers()) {
194 			return parameterName.toUpperCase();
195 		}
196 		else if (isStoresLowerCaseIdentifiers()) {
197 			return parameterName.toLowerCase();
198 		}
199 		else {
200 			return parameterName;
201 		}
202 	}
203 
204 	@Override
205 	public boolean byPassReturnParameter(String parameterName) {
206 		return false;
207 	}
208 
209 	@Override
210 	public SqlParameter createDefaultOutParameter(String parameterName, CallParameterMetaData meta) {
211 		return new SqlOutParameter(parameterName, meta.getSqlType());
212 	}
213 
214 	@Override
215 	public SqlParameter createDefaultInOutParameter(String parameterName, CallParameterMetaData meta) {
216 		return new SqlInOutParameter(parameterName, meta.getSqlType());
217 	}
218 
219 	@Override
220 	public SqlParameter createDefaultInParameter(String parameterName, CallParameterMetaData meta) {
221 		return new SqlParameter(parameterName, meta.getSqlType());
222 	}
223 
224 	@Override
225 	public String getUserName() {
226 		return this.userName;
227 	}
228 
229 	@Override
230 	public boolean isReturnResultSetSupported() {
231 		return true;
232 	}
233 
234 	@Override
235 	public boolean isRefCursorSupported() {
236 		return false;
237 	}
238 
239 	@Override
240 	public int getRefCursorSqlType() {
241 		return Types.OTHER;
242 	}
243 
244 	@Override
245 	public boolean isProcedureColumnMetaDataUsed() {
246 		return this.procedureColumnMetaDataUsed;
247 	}
248 
249 
250 	/**
251 	 * Specify whether the database supports the use of catalog name in procedure calls
252 	 */
253 	protected void setSupportsCatalogsInProcedureCalls(boolean supportsCatalogsInProcedureCalls) {
254 		this.supportsCatalogsInProcedureCalls = supportsCatalogsInProcedureCalls;
255 	}
256 
257 	/**
258 	 * Does the database support the use of catalog name in procedure calls
259 	 */
260 	@Override
261 	public boolean isSupportsCatalogsInProcedureCalls() {
262 		return this.supportsCatalogsInProcedureCalls;
263 	}
264 
265 	/**
266 	 * Specify whether the database supports the use of schema name in procedure calls
267 	 */
268 	protected void setSupportsSchemasInProcedureCalls(boolean supportsSchemasInProcedureCalls) {
269 		this.supportsSchemasInProcedureCalls = supportsSchemasInProcedureCalls;
270 	}
271 
272 	/**
273 	 * Does the database support the use of schema name in procedure calls
274 	 */
275 	@Override
276 	public boolean isSupportsSchemasInProcedureCalls() {
277 		return this.supportsSchemasInProcedureCalls;
278 	}
279 
280 	/**
281 	 * Specify whether the database uses upper case for identifiers
282 	 */
283 	protected void setStoresUpperCaseIdentifiers(boolean storesUpperCaseIdentifiers) {
284 		this.storesUpperCaseIdentifiers = storesUpperCaseIdentifiers;
285 	}
286 
287 	/**
288 	 * Does the database use upper case for identifiers
289 	 */
290 	protected boolean isStoresUpperCaseIdentifiers() {
291 		return this.storesUpperCaseIdentifiers;
292 	}
293 
294 	/**
295 	 * Specify whether the database uses lower case for identifiers
296 	 */
297 	protected void setStoresLowerCaseIdentifiers(boolean storesLowerCaseIdentifiers) {
298 		this.storesLowerCaseIdentifiers = storesLowerCaseIdentifiers;
299 	}
300 
301 	/**
302 	 * Does the database use lower case for identifiers
303 	 */
304 	protected boolean isStoresLowerCaseIdentifiers() {
305 		return this.storesLowerCaseIdentifiers;
306 	}
307 
308 
309 	/**
310 	 * Process the procedure column metadata
311 	 */
312 	private void processProcedureColumns(DatabaseMetaData databaseMetaData, String catalogName, String schemaName, String procedureName) {
313 		ResultSet procs = null;
314 		String metaDataCatalogName = metaDataCatalogNameToUse(catalogName);
315 		String metaDataSchemaName = metaDataSchemaNameToUse(schemaName);
316 		String metaDataProcedureName = procedureNameToUse(procedureName);
317 		if (logger.isDebugEnabled()) {
318 			logger.debug("Retrieving metadata for " + metaDataCatalogName + "/" +
319 					metaDataSchemaName + "/" + metaDataProcedureName);
320 		}
321 		try {
322 			procs = databaseMetaData.getProcedures(metaDataCatalogName, metaDataSchemaName, metaDataProcedureName);
323 			List<String> found = new ArrayList<String>();
324 			while (procs.next()) {
325 				found.add(procs.getString("PROCEDURE_CAT") + "." + procs.getString("PROCEDURE_SCHEM") +
326 						"." + procs.getString("PROCEDURE_NAME"));
327 			}
328 			procs.close();
329 			if (found.size() > 1) {
330 				throw new InvalidDataAccessApiUsageException("Unable to determine the correct call signature - " +
331 						"multiple procedures/functions/signatures for " + metaDataProcedureName + " found " + found);
332 			}
333 			if (found.size() < 1) {
334 				if (metaDataProcedureName.contains(".") && !StringUtils.hasText(metaDataCatalogName)) {
335 					String packageName = metaDataProcedureName.substring(0, metaDataProcedureName.indexOf("."));
336 					throw new InvalidDataAccessApiUsageException("Unable to determine the correct call signature for " +
337 							metaDataProcedureName + " - package name should be specified separately using " +
338 							"'.withCatalogName(\"" + packageName + "\")'");
339 				}
340 			}
341 
342 			procs = databaseMetaData.getProcedureColumns(
343 					metaDataCatalogName, metaDataSchemaName, metaDataProcedureName, null);
344 			while (procs.next()) {
345 				String columnName = procs.getString("COLUMN_NAME");
346 				int columnType = procs.getInt("COLUMN_TYPE");
347 				if (columnName == null && (
348 						columnType == DatabaseMetaData.procedureColumnIn  ||
349 						columnType == DatabaseMetaData.procedureColumnInOut ||
350 						columnType == DatabaseMetaData.procedureColumnOut)) {
351 					if (logger.isDebugEnabled()) {
352 						logger.debug("Skipping metadata for: " + columnType + " " + procs.getInt("DATA_TYPE") +
353 							" " + procs.getString("TYPE_NAME") + " " + procs.getBoolean("NULLABLE") +
354 							" (probably a member of a collection)"
355 						);
356 					}
357 				}
358 				else {
359 					CallParameterMetaData meta = new CallParameterMetaData(columnName, columnType,
360 							procs.getInt("DATA_TYPE"), procs.getString("TYPE_NAME"), procs.getBoolean("NULLABLE")
361 					);
362 					this.callParameterMetaData.add(meta);
363 					if (logger.isDebugEnabled()) {
364 						logger.debug("Retrieved metadata: " + meta.getParameterName() + " " +
365 								meta.getParameterType() + " " + meta.getSqlType() + " " +
366 								meta.getTypeName() + " " + meta.isNullable());
367 					}
368 				}
369 			}
370 		}
371 		catch (SQLException ex) {
372 			if (logger.isWarnEnabled()) {
373 				logger.warn("Error while retrieving metadata for procedure columns: " + ex);
374 			}
375 		}
376 		finally {
377 			try {
378 				if (procs != null) {
379 					procs.close();
380 				}
381 			}
382 			catch (SQLException ex) {
383 				if (logger.isWarnEnabled()) {
384 					logger.warn("Problem closing ResultSet for procedure column metadata: " + ex);
385 				}
386 			}
387 		}
388 	}
389 
390 }