1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
46
47
48
49
50
51 public class CallMetaDataContext {
52
53
54 protected final Log logger = LogFactory.getLog(getClass());
55
56
57 private String procedureName;
58
59
60 private String catalogName;
61
62
63 private String schemaName;
64
65
66 private List<SqlParameter> callParameters = new ArrayList<SqlParameter>();
67
68
69 private String actualFunctionReturnName;
70
71
72 private Set<String> limitedInParameterNames = new HashSet<String>();
73
74
75 private List<String> outParameterNames = new ArrayList<String>();
76
77
78 private boolean function = false;
79
80
81 private boolean returnValueRequired = false;
82
83
84 private boolean accessCallParameterMetaData = true;
85
86
87 private CallMetaDataProvider metaDataProvider;
88
89
90
91
92
93 public void setFunctionReturnName(String functionReturnName) {
94 this.actualFunctionReturnName = functionReturnName;
95 }
96
97
98
99
100 public String getFunctionReturnName() {
101 return (this.actualFunctionReturnName != null ? this.actualFunctionReturnName : "return");
102 }
103
104
105
106
107 public void setLimitedInParameterNames(Set<String> limitedInParameterNames) {
108 this.limitedInParameterNames = limitedInParameterNames;
109 }
110
111
112
113
114 public Set<String> getLimitedInParameterNames() {
115 return this.limitedInParameterNames;
116 }
117
118
119
120
121 public void setOutParameterNames(List<String> outParameterNames) {
122 this.outParameterNames = outParameterNames;
123 }
124
125
126
127
128 public List<String> getOutParameterNames() {
129 return this.outParameterNames;
130 }
131
132
133
134
135 public void setProcedureName(String procedureName) {
136 this.procedureName = procedureName;
137 }
138
139
140
141
142 public String getProcedureName() {
143 return this.procedureName;
144 }
145
146
147
148
149 public void setCatalogName(String catalogName) {
150 this.catalogName = catalogName;
151 }
152
153
154
155
156 public String getCatalogName() {
157 return this.catalogName;
158 }
159
160
161
162
163 public void setSchemaName(String schemaName) {
164 this.schemaName = schemaName;
165 }
166
167
168
169
170 public String getSchemaName() {
171 return this.schemaName;
172 }
173
174
175
176
177 public void setFunction(boolean function) {
178 this.function = function;
179 }
180
181
182
183
184 public boolean isFunction() {
185 return this.function;
186 }
187
188
189
190
191 public void setReturnValueRequired(boolean returnValueRequired) {
192 this.returnValueRequired = returnValueRequired;
193 }
194
195
196
197
198 public boolean isReturnValueRequired() {
199 return this.returnValueRequired;
200 }
201
202
203
204
205 public void setAccessCallParameterMetaData(boolean accessCallParameterMetaData) {
206 this.accessCallParameterMetaData = accessCallParameterMetaData;
207 }
208
209
210
211
212 public boolean isAccessCallParameterMetaData() {
213 return this.accessCallParameterMetaData;
214 }
215
216
217
218
219
220
221
222
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
240
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
256
257 public List<SqlParameter> getCallParameters() {
258 return this.callParameters;
259 }
260
261
262
263
264
265 public void initializeMetaData(DataSource dataSource) {
266 this.metaDataProvider = CallMetaDataProviderFactory.createMetaDataProvider(dataSource, this);
267 }
268
269
270
271
272
273
274
275 public void processParameters(List<SqlParameter> parameters) {
276 this.callParameters = reconcileParameters(parameters);
277 }
278
279
280
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
290 for (CallParameterMetaData meta : this.metaDataProvider.getCallParameterMetaData()) {
291 if (meta.getParameterType() != DatabaseMetaData.procedureColumnReturn) {
292 metaDataParameterNames.add(meta.getParameterName().toLowerCase());
293 }
294 }
295
296
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
428
429
430
431 public Map<String, Object> matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) {
432
433
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
491
492
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
559
560
561 public String createCallString() {
562 String callString;
563 int parameterCount = 0;
564 String catalogNameToUse;
565 String schemaNameToUse;
566
567
568
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 }