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.simple; 018 019import java.util.ArrayList; 020import java.util.LinkedHashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Set; 024import javax.sql.DataSource; 025 026import org.apache.commons.logging.Log; 027import org.apache.commons.logging.LogFactory; 028 029import org.springframework.dao.InvalidDataAccessApiUsageException; 030import org.springframework.jdbc.core.CallableStatementCreator; 031import org.springframework.jdbc.core.CallableStatementCreatorFactory; 032import org.springframework.jdbc.core.JdbcTemplate; 033import org.springframework.jdbc.core.RowMapper; 034import org.springframework.jdbc.core.SqlParameter; 035import org.springframework.jdbc.core.metadata.CallMetaDataContext; 036import org.springframework.jdbc.core.namedparam.SqlParameterSource; 037import org.springframework.util.Assert; 038import org.springframework.util.StringUtils; 039 040/** 041 * Abstract class to provide base functionality for easy stored procedure calls 042 * based on configuration options and database meta-data. 043 * 044 * <p>This class provides the base SPI for {@link SimpleJdbcCall}. 045 * 046 * @author Thomas Risberg 047 * @author Juergen Hoeller 048 * @since 2.5 049 */ 050public abstract class AbstractJdbcCall { 051 052 /** Logger available to subclasses */ 053 protected final Log logger = LogFactory.getLog(getClass()); 054 055 /** Lower-level class used to execute SQL */ 056 private final JdbcTemplate jdbcTemplate; 057 058 /** Context used to retrieve and manage database meta-data */ 059 private final CallMetaDataContext callMetaDataContext = new CallMetaDataContext(); 060 061 /** List of SqlParameter objects */ 062 private final List<SqlParameter> declaredParameters = new ArrayList<SqlParameter>(); 063 064 /** List of RefCursor/ResultSet RowMapper objects */ 065 private final Map<String, RowMapper<?>> declaredRowMappers = new LinkedHashMap<String, RowMapper<?>>(); 066 067 /** 068 * Has this operation been compiled? Compilation means at least checking 069 * that a DataSource or JdbcTemplate has been provided. 070 */ 071 private volatile boolean compiled = false; 072 073 /** The generated string used for call statement */ 074 private String callString; 075 076 /** 077 * A delegate enabling us to create CallableStatementCreators 078 * efficiently, based on this class's declared parameters. 079 */ 080 private CallableStatementCreatorFactory callableStatementFactory; 081 082 083 /** 084 * Constructor to be used when initializing using a {@link DataSource}. 085 * @param dataSource the DataSource to be used 086 */ 087 protected AbstractJdbcCall(DataSource dataSource) { 088 this.jdbcTemplate = new JdbcTemplate(dataSource); 089 } 090 091 /** 092 * Constructor to be used when initializing using a {@link JdbcTemplate}. 093 * @param jdbcTemplate the JdbcTemplate to use 094 */ 095 protected AbstractJdbcCall(JdbcTemplate jdbcTemplate) { 096 Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null"); 097 this.jdbcTemplate = jdbcTemplate; 098 } 099 100 101 /** 102 * Get the configured {@link JdbcTemplate}. 103 */ 104 public JdbcTemplate getJdbcTemplate() { 105 return this.jdbcTemplate; 106 } 107 108 /** 109 * Set the name of the stored procedure. 110 */ 111 public void setProcedureName(String procedureName) { 112 this.callMetaDataContext.setProcedureName(procedureName); 113 } 114 115 /** 116 * Get the name of the stored procedure. 117 */ 118 public String getProcedureName() { 119 return this.callMetaDataContext.getProcedureName(); 120 } 121 122 /** 123 * Set the names of in parameters to be used. 124 */ 125 public void setInParameterNames(Set<String> inParameterNames) { 126 this.callMetaDataContext.setLimitedInParameterNames(inParameterNames); 127 } 128 129 /** 130 * Get the names of in parameters to be used. 131 */ 132 public Set<String> getInParameterNames() { 133 return this.callMetaDataContext.getLimitedInParameterNames(); 134 } 135 136 /** 137 * Set the catalog name to use. 138 */ 139 public void setCatalogName(String catalogName) { 140 this.callMetaDataContext.setCatalogName(catalogName); 141 } 142 143 /** 144 * Get the catalog name used. 145 */ 146 public String getCatalogName() { 147 return this.callMetaDataContext.getCatalogName(); 148 } 149 150 /** 151 * Set the schema name to use. 152 */ 153 public void setSchemaName(String schemaName) { 154 this.callMetaDataContext.setSchemaName(schemaName); 155 } 156 157 /** 158 * Get the schema name used. 159 */ 160 public String getSchemaName() { 161 return this.callMetaDataContext.getSchemaName(); 162 } 163 164 /** 165 * Specify whether this call is a function call. 166 * The default is {@code false}. 167 */ 168 public void setFunction(boolean function) { 169 this.callMetaDataContext.setFunction(function); 170 } 171 172 /** 173 * Is this call a function call? 174 */ 175 public boolean isFunction() { 176 return this.callMetaDataContext.isFunction(); 177 } 178 179 /** 180 * Specify whether the call requires a return value. 181 * The default is {@code false}. 182 */ 183 public void setReturnValueRequired(boolean returnValueRequired) { 184 this.callMetaDataContext.setReturnValueRequired(returnValueRequired); 185 } 186 187 /** 188 * Does the call require a return value? 189 */ 190 public boolean isReturnValueRequired() { 191 return this.callMetaDataContext.isReturnValueRequired(); 192 } 193 194 /** 195 * Specify whether parameters should be bound by name. 196 * The default is {@code false}. 197 * @since 4.2 198 */ 199 public void setNamedBinding(boolean namedBinding) { 200 this.callMetaDataContext.setNamedBinding(namedBinding); 201 } 202 203 /** 204 * Should parameters be bound by name? 205 * @since 4.2 206 */ 207 public boolean isNamedBinding() { 208 return this.callMetaDataContext.isNamedBinding(); 209 } 210 211 /** 212 * Specify whether the parameter meta-data for the call should be used. 213 * The default is {@code true}. 214 */ 215 public void setAccessCallParameterMetaData(boolean accessCallParameterMetaData) { 216 this.callMetaDataContext.setAccessCallParameterMetaData(accessCallParameterMetaData); 217 } 218 219 /** 220 * Get the call string that should be used based on parameters and meta-data. 221 */ 222 public String getCallString() { 223 return this.callString; 224 } 225 226 /** 227 * Get the {@link CallableStatementCreatorFactory} being used 228 */ 229 protected CallableStatementCreatorFactory getCallableStatementFactory() { 230 return this.callableStatementFactory; 231 } 232 233 234 /** 235 * Add a declared parameter to the list of parameters for the call. 236 * <p>Only parameters declared as {@code SqlParameter} and {@code SqlInOutParameter} will 237 * be used to provide input values. This is different from the {@code StoredProcedure} 238 * class which - for backwards compatibility reasons - allows input values to be provided 239 * for parameters declared as {@code SqlOutParameter}. 240 * @param parameter the {@link SqlParameter} to add 241 */ 242 public void addDeclaredParameter(SqlParameter parameter) { 243 Assert.notNull(parameter, "The supplied parameter must not be null"); 244 if (!StringUtils.hasText(parameter.getName())) { 245 throw new InvalidDataAccessApiUsageException( 246 "You must specify a parameter name when declaring parameters for \"" + getProcedureName() + "\""); 247 } 248 this.declaredParameters.add(parameter); 249 if (logger.isDebugEnabled()) { 250 logger.debug("Added declared parameter for [" + getProcedureName() + "]: " + parameter.getName()); 251 } 252 } 253 254 /** 255 * Add a {@link org.springframework.jdbc.core.RowMapper} for the specified parameter or column. 256 * @param parameterName name of parameter or column 257 * @param rowMapper the RowMapper implementation to use 258 */ 259 public void addDeclaredRowMapper(String parameterName, RowMapper<?> rowMapper) { 260 this.declaredRowMappers.put(parameterName, rowMapper); 261 if (logger.isDebugEnabled()) { 262 logger.debug("Added row mapper for [" + getProcedureName() + "]: " + parameterName); 263 } 264 } 265 266 267 //------------------------------------------------------------------------- 268 // Methods handling compilation issues 269 //------------------------------------------------------------------------- 270 271 /** 272 * Compile this JdbcCall using provided parameters and meta-data plus other settings. 273 * <p>This finalizes the configuration for this object and subsequent attempts to compile are 274 * ignored. This will be implicitly called the first time an un-compiled call is executed. 275 * @throws org.springframework.dao.InvalidDataAccessApiUsageException if the object hasn't 276 * been correctly initialized, for example if no DataSource has been provided 277 */ 278 public final synchronized void compile() throws InvalidDataAccessApiUsageException { 279 if (!isCompiled()) { 280 if (getProcedureName() == null) { 281 throw new InvalidDataAccessApiUsageException("Procedure or Function name is required"); 282 } 283 try { 284 this.jdbcTemplate.afterPropertiesSet(); 285 } 286 catch (IllegalArgumentException ex) { 287 throw new InvalidDataAccessApiUsageException(ex.getMessage()); 288 } 289 compileInternal(); 290 this.compiled = true; 291 if (logger.isDebugEnabled()) { 292 logger.debug("SqlCall for " + (isFunction() ? "function" : "procedure") + 293 " [" + getProcedureName() + "] compiled"); 294 } 295 } 296 } 297 298 /** 299 * Delegate method to perform the actual compilation. 300 * <p>Subclasses can override this template method to perform their own compilation. 301 * Invoked after this base class's compilation is complete. 302 */ 303 protected void compileInternal() { 304 this.callMetaDataContext.initializeMetaData(getJdbcTemplate().getDataSource()); 305 306 // Iterate over the declared RowMappers and register the corresponding SqlParameter 307 for (Map.Entry<String, RowMapper<?>> entry : this.declaredRowMappers.entrySet()) { 308 SqlParameter resultSetParameter = 309 this.callMetaDataContext.createReturnResultSetParameter(entry.getKey(), entry.getValue()); 310 this.declaredParameters.add(resultSetParameter); 311 } 312 this.callMetaDataContext.processParameters(this.declaredParameters); 313 314 this.callString = this.callMetaDataContext.createCallString(); 315 if (logger.isDebugEnabled()) { 316 logger.debug("Compiled stored procedure. Call string is [" + this.callString + "]"); 317 } 318 319 this.callableStatementFactory = 320 new CallableStatementCreatorFactory(getCallString(), this.callMetaDataContext.getCallParameters()); 321 this.callableStatementFactory.setNativeJdbcExtractor(getJdbcTemplate().getNativeJdbcExtractor()); 322 323 onCompileInternal(); 324 } 325 326 /** 327 * Hook method that subclasses may override to react to compilation. 328 * This implementation does nothing. 329 */ 330 protected void onCompileInternal() { 331 } 332 333 /** 334 * Is this operation "compiled"? 335 * @return whether this operation is compiled and ready to use 336 */ 337 public boolean isCompiled() { 338 return this.compiled; 339 } 340 341 /** 342 * Check whether this operation has been compiled already; 343 * lazily compile it if not already compiled. 344 * <p>Automatically called by {@code doExecute}. 345 */ 346 protected void checkCompiled() { 347 if (!isCompiled()) { 348 logger.debug("JdbcCall call not compiled before execution - invoking compile"); 349 compile(); 350 } 351 } 352 353 354 //------------------------------------------------------------------------- 355 // Methods handling execution 356 //------------------------------------------------------------------------- 357 358 /** 359 * Delegate method that executes the call using the passed-in {@link SqlParameterSource}. 360 * @param parameterSource parameter names and values to be used in call 361 * @return Map of out parameters 362 */ 363 protected Map<String, Object> doExecute(SqlParameterSource parameterSource) { 364 checkCompiled(); 365 Map<String, Object> params = matchInParameterValuesWithCallParameters(parameterSource); 366 return executeCallInternal(params); 367 } 368 369 /** 370 * Delegate method that executes the call using the passed-in array of parameters. 371 * @param args array of parameter values. The order of values must match the order 372 * declared for the stored procedure. 373 * @return Map of out parameters 374 */ 375 protected Map<String, Object> doExecute(Object... args) { 376 checkCompiled(); 377 Map<String, ?> params = matchInParameterValuesWithCallParameters(args); 378 return executeCallInternal(params); 379 } 380 381 /** 382 * Delegate method that executes the call using the passed-in Map of parameters. 383 * @param args Map of parameter name and values 384 * @return Map of out parameters 385 */ 386 protected Map<String, Object> doExecute(Map<String, ?> args) { 387 checkCompiled(); 388 Map<String, ?> params = matchInParameterValuesWithCallParameters(args); 389 return executeCallInternal(params); 390 } 391 392 /** 393 * Delegate method to perform the actual call processing. 394 */ 395 private Map<String, Object> executeCallInternal(Map<String, ?> args) { 396 CallableStatementCreator csc = getCallableStatementFactory().newCallableStatementCreator(args); 397 if (logger.isDebugEnabled()) { 398 logger.debug("The following parameters are used for call " + getCallString() + " with " + args); 399 int i = 1; 400 for (SqlParameter param : getCallParameters()) { 401 logger.debug(i + ": " + param.getName() + ", SQL type "+ param.getSqlType() + ", type name " + 402 param.getTypeName() + ", parameter class [" + param.getClass().getName() + "]"); 403 i++; 404 } 405 } 406 return getJdbcTemplate().call(csc, getCallParameters()); 407 } 408 409 410 /** 411 * Get the name of a single out parameter or return value. 412 * Used for functions or procedures with one out parameter. 413 */ 414 protected String getScalarOutParameterName() { 415 return this.callMetaDataContext.getScalarOutParameterName(); 416 } 417 418 /** 419 * Get a List of all the call parameters to be used for call. 420 * This includes any parameters added based on meta-data processing. 421 */ 422 protected List<SqlParameter> getCallParameters() { 423 return this.callMetaDataContext.getCallParameters(); 424 } 425 426 /** 427 * Match the provided in parameter values with registered parameters and 428 * parameters defined via meta-data processing. 429 * @param parameterSource the parameter vakues provided as a {@link SqlParameterSource} 430 * @return Map with parameter names and values 431 */ 432 protected Map<String, Object> matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) { 433 return this.callMetaDataContext.matchInParameterValuesWithCallParameters(parameterSource); 434 } 435 436 /** 437 * Match the provided in parameter values with registered parameters and 438 * parameters defined via meta-data processing. 439 * @param args the parameter values provided as an array 440 * @return Map with parameter names and values 441 */ 442 private Map<String, ?> matchInParameterValuesWithCallParameters(Object[] args) { 443 return this.callMetaDataContext.matchInParameterValuesWithCallParameters(args); 444 } 445 446 /** 447 * Match the provided in parameter values with registered parameters and 448 * parameters defined via meta-data processing. 449 * @param args the parameter values provided in a Map 450 * @return Map with parameter names and values 451 */ 452 protected Map<String, ?> matchInParameterValuesWithCallParameters(Map<String, ?> args) { 453 return this.callMetaDataContext.matchInParameterValuesWithCallParameters(args); 454 } 455 456}