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.util.ArrayList;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.LinkedHashMap;
24  import java.util.List;
25  import java.util.Locale;
26  import java.util.Map;
27  import java.util.Set;
28  import javax.sql.DataSource;
29  
30  import org.apache.commons.logging.Log;
31  import org.apache.commons.logging.LogFactory;
32  
33  import org.springframework.dao.InvalidDataAccessApiUsageException;
34  import org.springframework.jdbc.core.RowMapper;
35  import org.springframework.jdbc.core.SqlOutParameter;
36  import org.springframework.jdbc.core.SqlParameter;
37  import org.springframework.jdbc.core.SqlParameterValue;
38  import org.springframework.jdbc.core.SqlReturnResultSet;
39  import org.springframework.jdbc.core.namedparam.SqlParameterSource;
40  import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
41  import org.springframework.jdbc.support.JdbcUtils;
42  import org.springframework.util.StringUtils;
43  
44  /**
45   * Class to manage context metadata used for the configuration and execution of the call.
46   *
47   * @author Thomas Risberg
48   * @author Juergen Hoeller
49   * @since 2.5
50   */
51  public class CallMetaDataContext {
52  
53  	/** Logger available to subclasses */
54  	protected final Log logger = LogFactory.getLog(getClass());
55  
56  	/** name of procedure to call **/
57  	private String procedureName;
58  
59  	/** name of catalog for call **/
60  	private String catalogName;
61  
62  	/** name of schema for call **/
63  	private String schemaName;
64  
65  	/** List of SqlParameter objects to be used in call execution */
66  	private List<SqlParameter> callParameters = new ArrayList<SqlParameter>();
67  
68  	/** Actual name to use for the return value in the output map */
69  	private String actualFunctionReturnName;
70  
71  	/** Set of in parameter names to exclude use for any not listed */
72  	private Set<String> limitedInParameterNames = new HashSet<String>();
73  
74  	/** List of SqlParameter names for out parameters */
75  	private List<String> outParameterNames = new ArrayList<String>();
76  
77  	/** Indicates whether this is a procedure or a function **/
78  	private boolean function = false;
79  
80  	/** Indicates whether this procedure's return value should be included  **/
81  	private boolean returnValueRequired = false;
82  
83  	/** Should we access call parameter meta data info or not */
84  	private boolean accessCallParameterMetaData = true;
85  
86  	/** The provider of call meta data */
87  	private CallMetaDataProvider metaDataProvider;
88  
89  
90  	/**
91  	 * Specify the name used for the return value of the function.
92  	 */
93  	public void setFunctionReturnName(String functionReturnName) {
94  		this.actualFunctionReturnName = functionReturnName;
95  	}
96  
97  	/**
98  	 * Get the name used for the return value of the function.
99  	 */
100 	public String getFunctionReturnName() {
101 		return (this.actualFunctionReturnName != null ? this.actualFunctionReturnName : "return");
102 	}
103 
104 	/**
105 	 * Specify a limited set of in parameters to be used.
106 	 */
107 	public void setLimitedInParameterNames(Set<String> limitedInParameterNames) {
108 		this.limitedInParameterNames = limitedInParameterNames;
109 	}
110 
111 	/**
112 	 * Get a limited set of in parameters to be used.
113 	 */
114 	public Set<String> getLimitedInParameterNames() {
115 		return this.limitedInParameterNames;
116 	}
117 
118 	/**
119 	 * Specify the names of the out parameters.
120 	 */
121 	public void setOutParameterNames(List<String> outParameterNames) {
122 		this.outParameterNames = outParameterNames;
123 	}
124 
125 	/**
126 	 * Get a list of the out parameter names.
127 	 */
128 	public List<String> getOutParameterNames() {
129 		return this.outParameterNames;
130 	}
131 
132 	/**
133 	 * Specify the name of the procedure.
134 	 */
135 	public void setProcedureName(String procedureName) {
136 		this.procedureName = procedureName;
137 	}
138 
139 	/**
140 	 * Get the name of the procedure.
141 	 */
142 	public String getProcedureName() {
143 		return this.procedureName;
144 	}
145 
146 	/**
147 	 * Specify the name of the catalog.
148 	 */
149 	public void setCatalogName(String catalogName) {
150 		this.catalogName = catalogName;
151 	}
152 
153 	/**
154 	 * Get the name of the catalog.
155 	 */
156 	public String getCatalogName() {
157 		return this.catalogName;
158 	}
159 
160 	/**
161 	 * Secify the name of the schema.
162 	 */
163 	public void setSchemaName(String schemaName) {
164 		this.schemaName = schemaName;
165 	}
166 
167 	/**
168 	 * Get the name of the schema.
169 	 */
170 	public String getSchemaName() {
171 		return this.schemaName;
172 	}
173 
174 	/**
175 	 * Specify whether this call is a function call.
176 	 */
177 	public void setFunction(boolean function) {
178 		this.function = function;
179 	}
180 
181 	/**
182 	 * Check whether this call is a function call.
183 	 */
184 	public boolean isFunction() {
185 		return this.function;
186 	}
187 
188 	/**
189 	 * Specify whether a return value is required.
190 	 */
191 	public void setReturnValueRequired(boolean returnValueRequired) {
192 		this.returnValueRequired = returnValueRequired;
193 	}
194 
195 	/**
196 	 * Check whether a return value is required.
197 	 */
198 	public boolean isReturnValueRequired() {
199 		return this.returnValueRequired;
200 	}
201 
202 	/**
203 	 * Specify whether call parameter metadata should be accessed.
204 	 */
205 	public void setAccessCallParameterMetaData(boolean accessCallParameterMetaData) {
206 		this.accessCallParameterMetaData = accessCallParameterMetaData;
207 	}
208 
209 	/**
210 	 * Check whether call parameter metadata should be accessed.
211 	 */
212 	public boolean isAccessCallParameterMetaData() {
213 		return this.accessCallParameterMetaData;
214 	}
215 
216 
217 	/**
218 	 * Create a ReturnResultSetParameter/SqlOutParameter depending on the support provided
219 	 * by the JDBC driver used for the database in use.
220 	 * @param parameterName the name of the parameter (also used as the name of the List returned in the output)
221 	 * @param rowMapper a RowMapper implementation used to map the data returned in the result set
222 	 * @return the appropriate SqlParameter
223 	 */
224 	public SqlParameter createReturnResultSetParameter(String parameterName, RowMapper<?> rowMapper) {
225 		if (this.metaDataProvider.isReturnResultSetSupported()) {
226 			return new SqlReturnResultSet(parameterName, rowMapper);
227 		}
228 		else {
229 			if (this.metaDataProvider.isRefCursorSupported()) {
230 				return new SqlOutParameter(parameterName, this.metaDataProvider.getRefCursorSqlType(), rowMapper);
231 			}
232 			else {
233 				throw new InvalidDataAccessApiUsageException("Return of a ResultSet from a stored procedure is not supported.");
234 			}
235 		}
236 	}
237 
238 	/**
239 	 * Get the name of the single out parameter for this call.
240 	 * If there are multiple parameters, the name of the first one will be returned.
241 	 */
242 	public String getScalarOutParameterName() {
243 		if (isFunction()) {
244 			return getFunctionReturnName();
245 		}
246 		else {
247 			if (this.outParameterNames.size() > 1) {
248 				logger.warn("Accessing single output value when procedure has more than one output parameter");
249 			}
250 			return (this.outParameterNames.size() > 0 ? this.outParameterNames.get(0) : null);
251 		}
252 	}
253 
254 	/**
255 	 * Get the List of SqlParameter objects to be used in call execution.
256 	 */
257 	public List<SqlParameter> getCallParameters() {
258 		return this.callParameters;
259 	}
260 
261 	/**
262 	 * Initialize this class with metadata from the database.
263 	 * @param dataSource the DataSource used to retrieve metadata
264 	 */
265 	public void initializeMetaData(DataSource dataSource) {
266 		this.metaDataProvider = CallMetaDataProviderFactory.createMetaDataProvider(dataSource, this);
267 	}
268 
269 	/**
270 	 * Process the list of parameters provided, and if procedure column metadata is used,
271 	 * the parameters will be matched against the metadata information and any missing
272 	 * ones will be automatically included.
273 	 * @param parameters the list of parameters to use as a base
274 	 */
275 	public void processParameters(List<SqlParameter> parameters) {
276 		this.callParameters = reconcileParameters(parameters);
277 	}
278 
279 	/**
280 	 * Reconcile the provided parameters with available metadata and add new ones where appropriate.
281 	 */
282 	protected List<SqlParameter> reconcileParameters(List<SqlParameter> parameters) {
283 		final List<SqlParameter> declaredReturnParameters = new ArrayList<SqlParameter>();
284 		final Map<String, SqlParameter> declaredParameters = new LinkedHashMap<String, SqlParameter>();
285 		boolean returnDeclared = false;
286 		List<String> outParameterNames = new ArrayList<String>();
287 		List<String> metaDataParameterNames = new ArrayList<String>();
288 
289 		// Get the names of the meta data parameters
290 		for (CallParameterMetaData meta : this.metaDataProvider.getCallParameterMetaData()) {
291 			if (meta.getParameterType() != DatabaseMetaData.procedureColumnReturn) {
292 				metaDataParameterNames.add(meta.getParameterName().toLowerCase());
293 			}
294 		}
295 
296 		// Separate implicit return parameters from explicit parameters...
297 		for (SqlParameter parameter : parameters) {
298 			if (parameter.isResultsParameter()) {
299 				declaredReturnParameters.add(parameter);
300 			}
301 			else {
302 				String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameter.getName()).toLowerCase();
303 				declaredParameters.put(parameterNameToMatch, parameter);
304 				if (parameter instanceof SqlOutParameter) {
305 					outParameterNames.add(parameter.getName());
306 					if (isFunction() && !metaDataParameterNames.contains(parameterNameToMatch)) {
307 						if (!returnDeclared) {
308 							if (logger.isDebugEnabled()) {
309 								logger.debug("Using declared out parameter '" + parameter.getName() + "' for function return value");
310 							}
311 							setFunctionReturnName(parameter.getName());
312 							returnDeclared = true;
313 						}
314 					}
315 				}
316 			}
317 		}
318 		setOutParameterNames(outParameterNames);
319 
320 		List<SqlParameter> workParameters = new ArrayList<SqlParameter>();
321 		workParameters.addAll(declaredReturnParameters);
322 
323 		if (!this.metaDataProvider.isProcedureColumnMetaDataUsed()) {
324 			workParameters.addAll(declaredParameters.values());
325 			return workParameters;
326 		}
327 
328 		Map<String, String> limitedInParamNamesMap = new HashMap<String, String>(this.limitedInParameterNames.size());
329 		for (String limitedParameterName : this.limitedInParameterNames) {
330 			limitedInParamNamesMap.put(
331 					this.metaDataProvider.parameterNameToUse(limitedParameterName).toLowerCase(), limitedParameterName);
332 		}
333 
334 		for (CallParameterMetaData meta : this.metaDataProvider.getCallParameterMetaData()) {
335 			String parNameToCheck = null;
336 			if (meta.getParameterName() != null) {
337 				parNameToCheck = this.metaDataProvider.parameterNameToUse(meta.getParameterName()).toLowerCase();
338 			}
339 			String parNameToUse = this.metaDataProvider.parameterNameToUse(meta.getParameterName());
340 			if (declaredParameters.containsKey(parNameToCheck) ||
341 					(meta.getParameterType() == DatabaseMetaData.procedureColumnReturn && returnDeclared)) {
342 				SqlParameter parameter;
343 				if (meta.getParameterType() == DatabaseMetaData.procedureColumnReturn) {
344 					parameter = declaredParameters.get(getFunctionReturnName());
345 					if (parameter == null && getOutParameterNames().size() > 0) {
346 						parameter = declaredParameters.get(getOutParameterNames().get(0).toLowerCase());
347 					}
348 					if (parameter == null) {
349 						throw new InvalidDataAccessApiUsageException(
350 								"Unable to locate declared parameter for function return value - " +
351 								" add a SqlOutParameter with name \"" + getFunctionReturnName() +"\"");
352 					}
353 					else {
354 						setFunctionReturnName(parameter.getName());
355 					}
356 				}
357 				else {
358 					parameter = declaredParameters.get(parNameToCheck);
359 				}
360 				if (parameter != null) {
361 					workParameters.add(parameter);
362 					if (logger.isDebugEnabled()) {
363 						logger.debug("Using declared parameter for: " +
364 								(parNameToUse == null ? getFunctionReturnName() : parNameToUse));
365 					}
366 				}
367 			}
368 			else {
369 				if (meta.getParameterType() == DatabaseMetaData.procedureColumnReturn) {
370 					if (!isFunction() && !isReturnValueRequired() &&
371 							this.metaDataProvider.byPassReturnParameter(meta.getParameterName())) {
372 						if (logger.isDebugEnabled()) {
373 							logger.debug("Bypassing metadata return parameter for: " + meta.getParameterName());
374 						}
375 					}
376 					else {
377 						String returnNameToUse =(StringUtils.hasLength(meta.getParameterName()) ?
378 								parNameToUse : getFunctionReturnName());
379 						workParameters.add(this.metaDataProvider.createDefaultOutParameter(returnNameToUse, meta));
380 						if (isFunction()) {
381 							setFunctionReturnName(returnNameToUse);
382 							outParameterNames.add(returnNameToUse);
383 						}
384 						if (logger.isDebugEnabled()) {
385 							logger.debug("Added metadata return parameter for: " + returnNameToUse);
386 						}
387 					}
388 				}
389 				else {
390 					if (meta.getParameterType() == DatabaseMetaData.procedureColumnOut) {
391 						workParameters.add(this.metaDataProvider.createDefaultOutParameter(parNameToUse, meta));
392 						outParameterNames.add(parNameToUse);
393 						if (logger.isDebugEnabled()) {
394 							logger.debug("Added metadata out parameter for: " + parNameToUse);
395 						}
396 					}
397 					else if (meta.getParameterType() == DatabaseMetaData.procedureColumnInOut) {
398 						workParameters.add(this.metaDataProvider.createDefaultInOutParameter(parNameToUse, meta));
399 						outParameterNames.add(parNameToUse);
400 						if (logger.isDebugEnabled()) {
401 							logger.debug("Added metadata in out parameter for: " + parNameToUse);
402 						}
403 					}
404 					else {
405 						if (this.limitedInParameterNames.isEmpty() ||
406 								limitedInParamNamesMap.containsKey(parNameToUse.toLowerCase())) {
407 							workParameters.add(this.metaDataProvider.createDefaultInParameter(parNameToUse, meta));
408 							if (logger.isDebugEnabled()) {
409 								logger.debug("Added metadata in parameter for: " + parNameToUse);
410 							}
411 						}
412 						else {
413 							if (logger.isDebugEnabled()) {
414 								logger.debug("Limited set of parameters " + limitedInParamNamesMap.keySet() +
415 										" skipped parameter for: " + parNameToUse);
416 							}
417 						}
418 					}
419 				}
420 			}
421 		}
422 
423 		return workParameters;
424 	}
425 
426 	/**
427 	 * Match input parameter values with the parameters declared to be used in the call.
428 	 * @param parameterSource the input values
429 	 * @return a Map containing the matched parameter names with the value taken from the input
430 	 */
431 	public Map<String, Object> matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) {
432 		// For parameter source lookups we need to provide case-insensitive lookup support
433 		// since the database metadata is not necessarily providing case sensitive parameter names.
434 		Map<String, String> caseInsensitiveParameterNames =
435 				SqlParameterSourceUtils.extractCaseInsensitiveParameterNames(parameterSource);
436 
437 		Map<String, String> callParameterNames = new HashMap<String, String>(this.callParameters.size());
438 		Map<String, Object> matchedParameters = new HashMap<String, Object>(this.callParameters.size());
439 		for (SqlParameter parameter : this.callParameters) {
440 			if (parameter.isInputValueProvided()) {
441 				String parameterName = parameter.getName();
442 				String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName);
443 				if (parameterNameToMatch != null) {
444 					callParameterNames.put(parameterNameToMatch.toLowerCase(), parameterName);
445 				}
446 				if (parameterName != null) {
447 					if (parameterSource.hasValue(parameterName)) {
448 						matchedParameters.put(parameterName, SqlParameterSourceUtils.getTypedValue(parameterSource, parameterName));
449 					}
450 					else {
451 						String lowerCaseName = parameterName.toLowerCase();
452 						if (parameterSource.hasValue(lowerCaseName)) {
453 							matchedParameters.put(parameterName, SqlParameterSourceUtils.getTypedValue(parameterSource, lowerCaseName));
454 						}
455 						else {
456 							String englishLowerCaseName = parameterName.toLowerCase(Locale.ENGLISH);
457 							if (parameterSource.hasValue(englishLowerCaseName)) {
458 								matchedParameters.put(parameterName, SqlParameterSourceUtils.getTypedValue(parameterSource, englishLowerCaseName));
459 							}
460 							else {
461 								String propertyName = JdbcUtils.convertUnderscoreNameToPropertyName(parameterName);
462 								if (parameterSource.hasValue(propertyName)) {
463 									matchedParameters.put(parameterName, SqlParameterSourceUtils.getTypedValue(parameterSource, propertyName));
464 								}
465 								else {
466 									if (caseInsensitiveParameterNames.containsKey(lowerCaseName)) {
467 										String sourceName = caseInsensitiveParameterNames.get(lowerCaseName);
468 										matchedParameters.put(parameterName, SqlParameterSourceUtils.getTypedValue(parameterSource, sourceName));
469 									}
470 									else {
471 										logger.warn("Unable to locate the corresponding parameter value for '" + parameterName +
472 												"' within the parameter values provided: " + caseInsensitiveParameterNames.values());
473 									}
474 								}
475 							}
476 						}
477 					}
478 				}
479 			}
480 		}
481 
482 		if (logger.isDebugEnabled()) {
483 			logger.debug("Matching " + caseInsensitiveParameterNames.values() + " with " + callParameterNames.values());
484 			logger.debug("Found match for " + matchedParameters.keySet());
485 		}
486 		return matchedParameters;
487 	}
488 
489 	/**
490 	 * Match input parameter values with the parameters declared to be used in the call.
491 	 * @param inParameters the input values
492 	 * @return a Map containing the matched parameter names with the value taken from the input
493 	 */
494 	public Map<String, ?> matchInParameterValuesWithCallParameters(Map<String, ?> inParameters) {
495 		if (!this.metaDataProvider.isProcedureColumnMetaDataUsed()) {
496 			return inParameters;
497 		}
498 		Map<String, String> callParameterNames = new HashMap<String, String>(this.callParameters.size());
499 		for (SqlParameter parameter : this.callParameters) {
500 			if (parameter.isInputValueProvided()) {
501 				String parameterName =  parameter.getName();
502 				String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName);
503 				if (parameterNameToMatch != null) {
504 					callParameterNames.put(parameterNameToMatch.toLowerCase(), parameterName);
505 				}
506 			}
507 		}
508 		Map<String, Object> matchedParameters = new HashMap<String, Object>(inParameters.size());
509 		for (String parameterName : inParameters.keySet()) {
510 			String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName);
511 			String callParameterName = callParameterNames.get(parameterNameToMatch.toLowerCase());
512 			if (callParameterName == null) {
513 				if (logger.isDebugEnabled()) {
514 					Object value = inParameters.get(parameterName);
515 					if (value instanceof SqlParameterValue) {
516 						value = ((SqlParameterValue)value).getValue();
517 					}
518 					if (value != null) {
519 						logger.debug("Unable to locate the corresponding IN or IN-OUT parameter for \"" + parameterName +
520 								"\" in the parameters used: " + callParameterNames.keySet());
521 					}
522 				}
523 			}
524 			else {
525 				matchedParameters.put(callParameterName, inParameters.get(parameterName));
526 			}
527 		}
528 		if (matchedParameters.size() < callParameterNames.size()) {
529 			for (String parameterName : callParameterNames.keySet()) {
530 				String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName);
531 				String callParameterName = callParameterNames.get(parameterNameToMatch.toLowerCase());
532 				if (!matchedParameters.containsKey(callParameterName)) {
533 					logger.warn("Unable to locate the corresponding parameter value for '" + parameterName +
534 							"' within the parameter values provided: " + inParameters.keySet());
535 				}
536 			}
537 		}
538 		if (logger.isDebugEnabled()) {
539 			logger.debug("Matching " + inParameters.keySet() + " with " + callParameterNames.values());
540 			logger.debug("Found match for " + matchedParameters.keySet());
541 		}
542 		return matchedParameters;
543 	}
544 
545 	public Map<String, ?> matchInParameterValuesWithCallParameters(Object[] parameterValues) {
546 		Map<String, Object> matchedParameters = new HashMap<String, Object>(parameterValues.length);
547 		int i = 0;
548 		for (SqlParameter parameter : this.callParameters) {
549 			if (parameter.isInputValueProvided()) {
550 				String parameterName =  parameter.getName();
551 				matchedParameters.put(parameterName, parameterValues[i++]);
552 			}
553 		}
554 		return matchedParameters;
555 	}
556 
557 	/**
558 	 * Build the call string based on configuration and metadata information.
559 	 * @return the call string to be used
560 	 */
561 	public String createCallString() {
562 		String callString;
563 		int parameterCount = 0;
564 		String catalogNameToUse;
565 		String schemaNameToUse;
566 
567 		// For Oracle where catalogs are not supported we need to reverse the schema name
568 		// and the catalog name since the cataog is used for the package name
569 		if (this.metaDataProvider.isSupportsSchemasInProcedureCalls() &&
570 				!this.metaDataProvider.isSupportsCatalogsInProcedureCalls()) {
571 			schemaNameToUse = this.metaDataProvider.catalogNameToUse(getCatalogName());
572 			catalogNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName());
573 		}
574 		else {
575 			catalogNameToUse = this.metaDataProvider.catalogNameToUse(getCatalogName());
576 			schemaNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName());
577 		}
578 		String procedureNameToUse = this.metaDataProvider.procedureNameToUse(getProcedureName());
579 		if (isFunction() || isReturnValueRequired()) {
580 			callString = "{? = call " +
581 					(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "") +
582 					(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "") +
583 					procedureNameToUse + "(";
584 			parameterCount = -1;
585 		}
586 		else {
587 			callString = "{call " +
588 					(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "") +
589 					(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "") +
590 					procedureNameToUse + "(";
591 		}
592 		for (SqlParameter parameter : this.callParameters) {
593 			if (!(parameter.isResultsParameter())) {
594 				if (parameterCount > 0) {
595 					callString += ", ";
596 				}
597 				if (parameterCount >= 0) {
598 					callString += "?";
599 				}
600 				parameterCount++;
601 			}
602 		}
603 		callString += ")}";
604 
605 		return callString;
606 	}
607 
608 }