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.util.ArrayList; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.LinkedHashMap; 024import java.util.List; 025import java.util.Locale; 026import java.util.Map; 027import java.util.Set; 028import javax.sql.DataSource; 029 030import org.apache.commons.logging.Log; 031import org.apache.commons.logging.LogFactory; 032 033import org.springframework.dao.InvalidDataAccessApiUsageException; 034import org.springframework.jdbc.core.RowMapper; 035import org.springframework.jdbc.core.SqlOutParameter; 036import org.springframework.jdbc.core.SqlParameter; 037import org.springframework.jdbc.core.SqlParameterValue; 038import org.springframework.jdbc.core.SqlReturnResultSet; 039import org.springframework.jdbc.core.namedparam.SqlParameterSource; 040import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; 041import org.springframework.jdbc.support.JdbcUtils; 042import org.springframework.util.StringUtils; 043 044/** 045 * Class to manage context meta-data used for the configuration 046 * and execution of a stored procedure call. 047 * 048 * @author Thomas Risberg 049 * @author Juergen Hoeller 050 * @author Kiril Nugmanov 051 * @since 2.5 052 */ 053public class CallMetaDataContext { 054 055 // Logger available to subclasses 056 protected final Log logger = LogFactory.getLog(getClass()); 057 058 // Name of procedure to call 059 private String procedureName; 060 061 // Name of catalog for call 062 private String catalogName; 063 064 // Name of schema for call 065 private String schemaName; 066 067 // List of SqlParameter objects to be used in call execution 068 private List<SqlParameter> callParameters = new ArrayList<SqlParameter>(); 069 070 // Actual name to use for the return value in the output map 071 private String actualFunctionReturnName; 072 073 // Set of in parameter names to exclude use for any not listed 074 private Set<String> limitedInParameterNames = new HashSet<String>(); 075 076 // List of SqlParameter names for out parameters 077 private List<String> outParameterNames = new ArrayList<String>(); 078 079 // Indicates whether this is a procedure or a function 080 private boolean function = false; 081 082 // Indicates whether this procedure's return value should be included 083 private boolean returnValueRequired = false; 084 085 // Should we access call parameter meta-data info or not 086 private boolean accessCallParameterMetaData = true; 087 088 // Should we bind parameter by name 089 private boolean namedBinding; 090 091 // The provider of call meta-data 092 private CallMetaDataProvider metaDataProvider; 093 094 095 /** 096 * Specify the name used for the return value of the function. 097 */ 098 public void setFunctionReturnName(String functionReturnName) { 099 this.actualFunctionReturnName = functionReturnName; 100 } 101 102 /** 103 * Get the name used for the return value of the function. 104 */ 105 public String getFunctionReturnName() { 106 return (this.actualFunctionReturnName != null ? this.actualFunctionReturnName : "return"); 107 } 108 109 /** 110 * Specify a limited set of in parameters to be used. 111 */ 112 public void setLimitedInParameterNames(Set<String> limitedInParameterNames) { 113 this.limitedInParameterNames = limitedInParameterNames; 114 } 115 116 /** 117 * Get a limited set of in parameters to be used. 118 */ 119 public Set<String> getLimitedInParameterNames() { 120 return this.limitedInParameterNames; 121 } 122 123 /** 124 * Specify the names of the out parameters. 125 */ 126 public void setOutParameterNames(List<String> outParameterNames) { 127 this.outParameterNames = outParameterNames; 128 } 129 130 /** 131 * Get a list of the out parameter names. 132 */ 133 public List<String> getOutParameterNames() { 134 return this.outParameterNames; 135 } 136 137 /** 138 * Specify the name of the procedure. 139 */ 140 public void setProcedureName(String procedureName) { 141 this.procedureName = procedureName; 142 } 143 144 /** 145 * Get the name of the procedure. 146 */ 147 public String getProcedureName() { 148 return this.procedureName; 149 } 150 151 /** 152 * Specify the name of the catalog. 153 */ 154 public void setCatalogName(String catalogName) { 155 this.catalogName = catalogName; 156 } 157 158 /** 159 * Get the name of the catalog. 160 */ 161 public String getCatalogName() { 162 return this.catalogName; 163 } 164 165 /** 166 * Secify the name of the schema. 167 */ 168 public void setSchemaName(String schemaName) { 169 this.schemaName = schemaName; 170 } 171 172 /** 173 * Get the name of the schema. 174 */ 175 public String getSchemaName() { 176 return this.schemaName; 177 } 178 179 /** 180 * Specify whether this call is a function call. 181 */ 182 public void setFunction(boolean function) { 183 this.function = function; 184 } 185 186 /** 187 * Check whether this call is a function call. 188 */ 189 public boolean isFunction() { 190 return this.function; 191 } 192 193 /** 194 * Specify whether a return value is required. 195 */ 196 public void setReturnValueRequired(boolean returnValueRequired) { 197 this.returnValueRequired = returnValueRequired; 198 } 199 200 /** 201 * Check whether a return value is required. 202 */ 203 public boolean isReturnValueRequired() { 204 return this.returnValueRequired; 205 } 206 207 /** 208 * Specify whether call parameter meta-data should be accessed. 209 */ 210 public void setAccessCallParameterMetaData(boolean accessCallParameterMetaData) { 211 this.accessCallParameterMetaData = accessCallParameterMetaData; 212 } 213 214 /** 215 * Check whether call parameter meta-data should be accessed. 216 */ 217 public boolean isAccessCallParameterMetaData() { 218 return this.accessCallParameterMetaData; 219 } 220 221 /** 222 * Specify whether parameters should be bound by name. 223 * @since 4.2 224 */ 225 public void setNamedBinding(boolean namedBinding) { 226 this.namedBinding = namedBinding; 227 } 228 229 /** 230 * Check whether parameters should be bound by name. 231 * @since 4.2 232 */ 233 public boolean isNamedBinding() { 234 return this.namedBinding; 235 } 236 237 238 /** 239 * Initialize this class with meta-data from the database. 240 * @param dataSource the DataSource used to retrieve meta-data 241 */ 242 public void initializeMetaData(DataSource dataSource) { 243 this.metaDataProvider = CallMetaDataProviderFactory.createMetaDataProvider(dataSource, this); 244 } 245 246 /** 247 * Create a ReturnResultSetParameter/SqlOutParameter depending on the support provided 248 * by the JDBC driver used for the database in use. 249 * @param parameterName the name of the parameter (also used as the name of the List returned in the output) 250 * @param rowMapper a RowMapper implementation used to map the data returned in the result set 251 * @return the appropriate SqlParameter 252 */ 253 public SqlParameter createReturnResultSetParameter(String parameterName, RowMapper<?> rowMapper) { 254 if (this.metaDataProvider.isReturnResultSetSupported()) { 255 return new SqlReturnResultSet(parameterName, rowMapper); 256 } 257 else { 258 if (this.metaDataProvider.isRefCursorSupported()) { 259 return new SqlOutParameter(parameterName, this.metaDataProvider.getRefCursorSqlType(), rowMapper); 260 } 261 else { 262 throw new InvalidDataAccessApiUsageException( 263 "Return of a ResultSet from a stored procedure is not supported"); 264 } 265 } 266 } 267 268 /** 269 * Get the name of the single out parameter for this call. 270 * If there are multiple parameters, the name of the first one will be returned. 271 */ 272 public String getScalarOutParameterName() { 273 if (isFunction()) { 274 return getFunctionReturnName(); 275 } 276 else { 277 if (this.outParameterNames.size() > 1) { 278 logger.warn("Accessing single output value when procedure has more than one output parameter"); 279 } 280 return (!this.outParameterNames.isEmpty() ? this.outParameterNames.get(0) : null); 281 } 282 } 283 284 /** 285 * Get the List of SqlParameter objects to be used in call execution. 286 */ 287 public List<SqlParameter> getCallParameters() { 288 return this.callParameters; 289 } 290 291 /** 292 * Process the list of parameters provided, and if procedure column meta-data is used, 293 * the parameters will be matched against the meta-data information and any missing 294 * ones will be automatically included. 295 * @param parameters the list of parameters to use as a base 296 */ 297 public void processParameters(List<SqlParameter> parameters) { 298 this.callParameters = reconcileParameters(parameters); 299 } 300 301 /** 302 * Reconcile the provided parameters with available meta-data and add new ones where appropriate. 303 */ 304 protected List<SqlParameter> reconcileParameters(List<SqlParameter> parameters) { 305 final List<SqlParameter> declaredReturnParams = new ArrayList<SqlParameter>(); 306 final Map<String, SqlParameter> declaredParams = new LinkedHashMap<String, SqlParameter>(); 307 boolean returnDeclared = false; 308 List<String> outParamNames = new ArrayList<String>(); 309 List<String> metaDataParamNames = new ArrayList<String>(); 310 311 // Get the names of the meta-data parameters 312 for (CallParameterMetaData meta : this.metaDataProvider.getCallParameterMetaData()) { 313 if (!meta.isReturnParameter()) { 314 metaDataParamNames.add(meta.getParameterName().toLowerCase()); 315 } 316 } 317 318 // Separate implicit return parameters from explicit parameters... 319 for (SqlParameter param : parameters) { 320 if (param.isResultsParameter()) { 321 declaredReturnParams.add(param); 322 } 323 else { 324 String paramName = param.getName(); 325 if (paramName == null) { 326 throw new IllegalArgumentException("Anonymous parameters not supported for calls - " + 327 "please specify a name for the parameter of SQL type " + param.getSqlType()); 328 } 329 String paramNameToMatch = this.metaDataProvider.parameterNameToUse(paramName).toLowerCase(); 330 declaredParams.put(paramNameToMatch, param); 331 if (param instanceof SqlOutParameter) { 332 outParamNames.add(paramName); 333 if (isFunction() && !metaDataParamNames.contains(paramNameToMatch) && !returnDeclared) { 334 if (logger.isDebugEnabled()) { 335 logger.debug("Using declared out parameter '" + paramName + "' for function return value"); 336 } 337 setFunctionReturnName(paramName); 338 returnDeclared = true; 339 } 340 } 341 } 342 } 343 setOutParameterNames(outParamNames); 344 345 List<SqlParameter> workParams = new ArrayList<SqlParameter>(); 346 workParams.addAll(declaredReturnParams); 347 if (!this.metaDataProvider.isProcedureColumnMetaDataUsed()) { 348 workParams.addAll(declaredParams.values()); 349 return workParams; 350 } 351 352 Map<String, String> limitedInParamNamesMap = new HashMap<String, String>(this.limitedInParameterNames.size()); 353 for (String limitedParamName : this.limitedInParameterNames) { 354 limitedInParamNamesMap.put( 355 this.metaDataProvider.parameterNameToUse(limitedParamName).toLowerCase(), limitedParamName); 356 } 357 358 for (CallParameterMetaData meta : this.metaDataProvider.getCallParameterMetaData()) { 359 String paramNameToCheck = null; 360 if (meta.getParameterName() != null) { 361 paramNameToCheck = this.metaDataProvider.parameterNameToUse(meta.getParameterName()).toLowerCase(); 362 } 363 String paramNameToUse = this.metaDataProvider.parameterNameToUse(meta.getParameterName()); 364 if (declaredParams.containsKey(paramNameToCheck) || (meta.isReturnParameter() && returnDeclared)) { 365 SqlParameter param; 366 if (meta.isReturnParameter()) { 367 param = declaredParams.get(getFunctionReturnName()); 368 if (param == null && !getOutParameterNames().isEmpty()) { 369 param = declaredParams.get(getOutParameterNames().get(0).toLowerCase()); 370 } 371 if (param == null) { 372 throw new InvalidDataAccessApiUsageException( 373 "Unable to locate declared parameter for function return value - " + 374 " add a SqlOutParameter with name '" + getFunctionReturnName() + "'"); 375 } 376 else { 377 setFunctionReturnName(param.getName()); 378 } 379 } 380 else { 381 param = declaredParams.get(paramNameToCheck); 382 } 383 if (param != null) { 384 workParams.add(param); 385 if (logger.isDebugEnabled()) { 386 logger.debug("Using declared parameter for '" + 387 (paramNameToUse != null ? paramNameToUse : getFunctionReturnName()) + "'"); 388 } 389 } 390 } 391 else { 392 if (meta.isReturnParameter()) { 393 // DatabaseMetaData.procedureColumnReturn or possibly procedureColumnResult 394 if (!isFunction() && !isReturnValueRequired() && 395 this.metaDataProvider.byPassReturnParameter(meta.getParameterName())) { 396 if (logger.isDebugEnabled()) { 397 logger.debug("Bypassing meta-data return parameter for '" + meta.getParameterName() + "'"); 398 } 399 } 400 else { 401 String returnNameToUse = 402 (StringUtils.hasLength(meta.getParameterName()) ? paramNameToUse : getFunctionReturnName()); 403 workParams.add(this.metaDataProvider.createDefaultOutParameter(returnNameToUse, meta)); 404 if (isFunction()) { 405 setFunctionReturnName(returnNameToUse); 406 outParamNames.add(returnNameToUse); 407 } 408 if (logger.isDebugEnabled()) { 409 logger.debug("Added meta-data return parameter for '" + returnNameToUse + "'"); 410 } 411 } 412 } 413 else { 414 if (meta.getParameterType() == DatabaseMetaData.procedureColumnOut) { 415 workParams.add(this.metaDataProvider.createDefaultOutParameter(paramNameToUse, meta)); 416 outParamNames.add(paramNameToUse); 417 if (logger.isDebugEnabled()) { 418 logger.debug("Added meta-data out parameter for '" + paramNameToUse + "'"); 419 } 420 } 421 else if (meta.getParameterType() == DatabaseMetaData.procedureColumnInOut) { 422 workParams.add(this.metaDataProvider.createDefaultInOutParameter(paramNameToUse, meta)); 423 outParamNames.add(paramNameToUse); 424 if (logger.isDebugEnabled()) { 425 logger.debug("Added meta-data in-out parameter for '" + paramNameToUse + "'"); 426 } 427 } 428 else { 429 if (this.limitedInParameterNames.isEmpty() || 430 limitedInParamNamesMap.containsKey(paramNameToUse.toLowerCase())) { 431 workParams.add(this.metaDataProvider.createDefaultInParameter(paramNameToUse, meta)); 432 if (logger.isDebugEnabled()) { 433 logger.debug("Added meta-data in parameter for '" + paramNameToUse + "'"); 434 } 435 } 436 else { 437 if (logger.isDebugEnabled()) { 438 logger.debug("Limited set of parameters " + limitedInParamNamesMap.keySet() + 439 " skipped parameter for '" + paramNameToUse + "'"); 440 } 441 } 442 } 443 } 444 } 445 } 446 447 return workParams; 448 } 449 450 /** 451 * Match input parameter values with the parameters declared to be used in the call. 452 * @param parameterSource the input values 453 * @return a Map containing the matched parameter names with the value taken from the input 454 */ 455 public Map<String, Object> matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) { 456 // For parameter source lookups we need to provide case-insensitive lookup support 457 // since the database meta-data is not necessarily providing case sensitive parameter names. 458 Map<String, String> caseInsensitiveParameterNames = 459 SqlParameterSourceUtils.extractCaseInsensitiveParameterNames(parameterSource); 460 461 Map<String, String> callParameterNames = new HashMap<String, String>(this.callParameters.size()); 462 Map<String, Object> matchedParameters = new HashMap<String, Object>(this.callParameters.size()); 463 for (SqlParameter parameter : this.callParameters) { 464 if (parameter.isInputValueProvided()) { 465 String parameterName = parameter.getName(); 466 String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName); 467 if (parameterNameToMatch != null) { 468 callParameterNames.put(parameterNameToMatch.toLowerCase(), parameterName); 469 } 470 if (parameterName != null) { 471 if (parameterSource.hasValue(parameterName)) { 472 matchedParameters.put(parameterName, 473 SqlParameterSourceUtils.getTypedValue(parameterSource, parameterName)); 474 } 475 else { 476 String lowerCaseName = parameterName.toLowerCase(); 477 if (parameterSource.hasValue(lowerCaseName)) { 478 matchedParameters.put(parameterName, 479 SqlParameterSourceUtils.getTypedValue(parameterSource, lowerCaseName)); 480 } 481 else { 482 String englishLowerCaseName = parameterName.toLowerCase(Locale.ENGLISH); 483 if (parameterSource.hasValue(englishLowerCaseName)) { 484 matchedParameters.put(parameterName, 485 SqlParameterSourceUtils.getTypedValue(parameterSource, englishLowerCaseName)); 486 } 487 else { 488 String propertyName = JdbcUtils.convertUnderscoreNameToPropertyName(parameterName); 489 if (parameterSource.hasValue(propertyName)) { 490 matchedParameters.put(parameterName, 491 SqlParameterSourceUtils.getTypedValue(parameterSource, propertyName)); 492 } 493 else { 494 if (caseInsensitiveParameterNames.containsKey(lowerCaseName)) { 495 String sourceName = caseInsensitiveParameterNames.get(lowerCaseName); 496 matchedParameters.put(parameterName, 497 SqlParameterSourceUtils.getTypedValue(parameterSource, sourceName)); 498 } 499 else if (logger.isWarnEnabled()) { 500 logger.warn("Unable to locate the corresponding parameter value for '" + 501 parameterName + "' within the parameter values provided: " + 502 caseInsensitiveParameterNames.values()); 503 } 504 } 505 } 506 } 507 } 508 } 509 } 510 } 511 512 if (logger.isDebugEnabled()) { 513 logger.debug("Matching " + caseInsensitiveParameterNames.values() + " with " + callParameterNames.values()); 514 logger.debug("Found match for " + matchedParameters.keySet()); 515 } 516 return matchedParameters; 517 } 518 519 /** 520 * Match input parameter values with the parameters declared to be used in the call. 521 * @param inParameters the input values 522 * @return a Map containing the matched parameter names with the value taken from the input 523 */ 524 public Map<String, ?> matchInParameterValuesWithCallParameters(Map<String, ?> inParameters) { 525 if (!this.metaDataProvider.isProcedureColumnMetaDataUsed()) { 526 return inParameters; 527 } 528 529 Map<String, String> callParameterNames = new HashMap<String, String>(this.callParameters.size()); 530 for (SqlParameter parameter : this.callParameters) { 531 if (parameter.isInputValueProvided()) { 532 String parameterName = parameter.getName(); 533 String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName); 534 if (parameterNameToMatch != null) { 535 callParameterNames.put(parameterNameToMatch.toLowerCase(), parameterName); 536 } 537 } 538 } 539 540 Map<String, Object> matchedParameters = new HashMap<String, Object>(inParameters.size()); 541 for (String parameterName : inParameters.keySet()) { 542 String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName); 543 String callParameterName = callParameterNames.get(parameterNameToMatch.toLowerCase()); 544 if (callParameterName == null) { 545 if (logger.isDebugEnabled()) { 546 Object value = inParameters.get(parameterName); 547 if (value instanceof SqlParameterValue) { 548 value = ((SqlParameterValue) value).getValue(); 549 } 550 if (value != null) { 551 logger.debug("Unable to locate the corresponding IN or IN-OUT parameter for \"" + 552 parameterName + "\" in the parameters used: " + callParameterNames.keySet()); 553 } 554 } 555 } 556 else { 557 matchedParameters.put(callParameterName, inParameters.get(parameterName)); 558 } 559 } 560 561 if (matchedParameters.size() < callParameterNames.size()) { 562 for (String parameterName : callParameterNames.keySet()) { 563 String parameterNameToMatch = this.metaDataProvider.parameterNameToUse(parameterName); 564 String callParameterName = callParameterNames.get(parameterNameToMatch.toLowerCase()); 565 if (!matchedParameters.containsKey(callParameterName) && logger.isWarnEnabled()) { 566 logger.warn("Unable to locate the corresponding parameter value for '" + parameterName + 567 "' within the parameter values provided: " + inParameters.keySet()); 568 } 569 } 570 } 571 572 if (logger.isDebugEnabled()) { 573 logger.debug("Matching " + inParameters.keySet() + " with " + callParameterNames.values()); 574 logger.debug("Found match for " + matchedParameters.keySet()); 575 } 576 return matchedParameters; 577 } 578 579 public Map<String, ?> matchInParameterValuesWithCallParameters(Object[] parameterValues) { 580 Map<String, Object> matchedParameters = new HashMap<String, Object>(parameterValues.length); 581 int i = 0; 582 for (SqlParameter parameter : this.callParameters) { 583 if (parameter.isInputValueProvided()) { 584 String parameterName = parameter.getName(); 585 matchedParameters.put(parameterName, parameterValues[i++]); 586 } 587 } 588 return matchedParameters; 589 } 590 591 /** 592 * Build the call string based on configuration and meta-data information. 593 * @return the call string to be used 594 */ 595 public String createCallString() { 596 StringBuilder callString; 597 int parameterCount = 0; 598 String catalogNameToUse; 599 String schemaNameToUse; 600 601 // For Oracle where catalogs are not supported we need to reverse the schema name 602 // and the catalog name since the cataog is used for the package name 603 if (this.metaDataProvider.isSupportsSchemasInProcedureCalls() && 604 !this.metaDataProvider.isSupportsCatalogsInProcedureCalls()) { 605 schemaNameToUse = this.metaDataProvider.catalogNameToUse(getCatalogName()); 606 catalogNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName()); 607 } 608 else { 609 catalogNameToUse = this.metaDataProvider.catalogNameToUse(getCatalogName()); 610 schemaNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName()); 611 } 612 613 String procedureNameToUse = this.metaDataProvider.procedureNameToUse(getProcedureName()); 614 if (isFunction() || isReturnValueRequired()) { 615 callString = new StringBuilder().append("{? = call "). 616 append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : ""). 617 append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : ""). 618 append(procedureNameToUse).append("("); 619 parameterCount = -1; 620 } 621 else { 622 callString = new StringBuilder().append("{call "). 623 append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : ""). 624 append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : ""). 625 append(procedureNameToUse).append("("); 626 } 627 628 for (SqlParameter parameter : this.callParameters) { 629 if (!parameter.isResultsParameter()) { 630 if (parameterCount > 0) { 631 callString.append(", "); 632 } 633 if (parameterCount >= 0) { 634 callString.append(createParameterBinding(parameter)); 635 } 636 parameterCount++; 637 } 638 } 639 callString.append(")}"); 640 641 return callString.toString(); 642 } 643 644 /** 645 * Build the parameter binding fragment. 646 * @param parameter call parameter 647 * @return parameter binding fragment 648 * @since 4.2 649 */ 650 protected String createParameterBinding(SqlParameter parameter) { 651 return (isNamedBinding() ? parameter.getName() + " => ?" : "?"); 652 } 653 654}