001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.jdbc.core.metadata;
018:
019: import java.sql.DatabaseMetaData;
020: import java.util.ArrayList;
021: import java.util.HashMap;
022: import java.util.HashSet;
023: import java.util.LinkedHashMap;
024: import java.util.List;
025: import java.util.Map;
026: import java.util.Set;
027:
028: import javax.sql.DataSource;
029:
030: import org.apache.commons.logging.Log;
031: import org.apache.commons.logging.LogFactory;
032:
033: import org.springframework.dao.InvalidDataAccessApiUsageException;
034: import org.springframework.jdbc.core.RowMapper;
035: import org.springframework.jdbc.core.SqlOutParameter;
036: import org.springframework.jdbc.core.SqlParameter;
037: import org.springframework.jdbc.core.SqlReturnResultSet;
038: import org.springframework.jdbc.core.namedparam.SqlParameterSource;
039: import org.springframework.jdbc.support.JdbcUtils;
040:
041: /**
042: * Class to manage context metadata used for the configuration and execution of the call.
043: *
044: * @author Thomas Risberg
045: * @since 2.5
046: */
047: public class CallMetaDataContext {
048:
049: /** Logger available to subclasses */
050: protected final Log logger = LogFactory.getLog(getClass());
051:
052: /** name of procedure to call **/
053: private String procedureName;
054:
055: /** name of catalog for call **/
056: private String catalogName;
057:
058: /** name of schema for call **/
059: private String schemaName;
060:
061: /** List of SqlParameter objects to be used in call execution */
062: private List<SqlParameter> callParameters = new ArrayList<SqlParameter>();
063:
064: /** name to use for the return value in the output map */
065: private String functionReturnName = "return";
066:
067: /** Set of in parameter names to exclude use for any not listed */
068: private Set<String> limitedInParameterNames = new HashSet<String>();
069:
070: /** List of SqlParameter names for out parameters */
071: private List<String> outParameterNames = new ArrayList<String>();
072:
073: /** should we access call parameter meta data info or not */
074: private boolean accessCallParameterMetaData = true;
075:
076: /** indicates whether this is a procedure or a function **/
077: private boolean function;
078:
079: /** indicates whether this procedure's return value should be included **/
080: private boolean returnValueRequired;
081:
082: /** the provider of call meta data */
083: private CallMetaDataProvider metaDataProvider;
084:
085: /**
086: * Specify the name used for the return value of the function.
087: */
088: public void setFunctionReturnName(String functionReturnName) {
089: this .functionReturnName = functionReturnName;
090: }
091:
092: /**
093: * Get the name used for the return value of the function.
094: */
095: public String getFunctionReturnName() {
096: return this .functionReturnName;
097: }
098:
099: /**
100: * Specify a limited set of in parameters to be used.
101: */
102: public void setLimitedInParameterNames(
103: Set<String> limitedInParameterNames) {
104: this .limitedInParameterNames = limitedInParameterNames;
105: }
106:
107: /**
108: * Get a limited set of in parameters to be used.
109: */
110: public Set<String> getLimitedInParameterNames() {
111: return this .limitedInParameterNames;
112: }
113:
114: /**
115: * Specify the names of the out parameters.
116: */
117: public void setOutParameterNames(List<String> outParameterNames) {
118: this .outParameterNames = outParameterNames;
119: }
120:
121: /**
122: * Get a list of the out parameter names.
123: */
124: public List<String> getOutParameterNames() {
125: return this .outParameterNames;
126: }
127:
128: /**
129: * Specify the name of the procedure.
130: */
131: public void setProcedureName(String procedureName) {
132: this .procedureName = procedureName;
133: }
134:
135: /**
136: * Get the name of the procedure.
137: */
138: public String getProcedureName() {
139: return this .procedureName;
140: }
141:
142: /**
143: * Specify the name of the catalog.
144: */
145: public void setCatalogName(String catalogName) {
146: this .catalogName = catalogName;
147: }
148:
149: /**
150: * Get the name of the catalog.
151: */
152: public String getCatalogName() {
153: return this .catalogName;
154: }
155:
156: /**
157: * Secify the name of the schema.
158: */
159: public void setSchemaName(String schemaName) {
160: this .schemaName = schemaName;
161: }
162:
163: /**
164: * Get the name of the schema.
165: */
166: public String getSchemaName() {
167: return this .schemaName;
168: }
169:
170: /**
171: * Specify whether this call is a function call.
172: */
173: public void setFunction(boolean function) {
174: this .function = function;
175: }
176:
177: /**
178: * Check whether this call is a function call.
179: */
180: public boolean isFunction() {
181: return this .function;
182: }
183:
184: /**
185: * Specify whether a return value is required.
186: */
187: public void setReturnValueRequired(boolean returnValueRequired) {
188: this .returnValueRequired = returnValueRequired;
189: }
190:
191: /**
192: * Check whether a return value is required.
193: */
194: public boolean isReturnValueRequired() {
195: return this .returnValueRequired;
196: }
197:
198: /**
199: * Specify whether call parameter metadata should be accessed.
200: */
201: public void setAccessCallParameterMetaData(
202: boolean accessCallParameterMetaData) {
203: this .accessCallParameterMetaData = accessCallParameterMetaData;
204: }
205:
206: /**
207: * Check whether call parameter metadata should be accessed.
208: */
209: public boolean isAccessCallParameterMetaData() {
210: return this .accessCallParameterMetaData;
211: }
212:
213: /**
214: * Create a ReturnResultSetParameter/SqlOutParameter depending on the support provided by the JDBC driver
215: * used for the database in use.
216: * @param parameterName the name of the parameter. Also be used as the name of the List returned in the output.
217: * @param rowMapper a RowMapper iplementation used to map the data retuned in the result set
218: * @return the appropriate SqlParameter
219: */
220: public SqlParameter createReturnResultSetParameter(
221: String parameterName, RowMapper rowMapper) {
222: if (this .metaDataProvider.isReturnResultSetSupported()) {
223: return new SqlReturnResultSet(parameterName, rowMapper);
224: } else {
225: if (this .metaDataProvider.isRefCursorSupported()) {
226: return new SqlOutParameter(parameterName,
227: this .metaDataProvider.getRefCursorSqlType(),
228: rowMapper);
229: } else {
230: throw new InvalidDataAccessApiUsageException(
231: "Return of a ResultSet from a stored procedure is not supported.");
232: }
233: }
234: }
235:
236: /**
237: * Get the name of the single out parameter for this call. If there are multiple parameters then the name of
238: * the first one is returned.
239: */
240: public String getScalarOutParameterName() {
241: if (isFunction()) {
242: return this .functionReturnName;
243: } else {
244: if (this .outParameterNames.size() > 1) {
245: logger
246: .warn("Accessing single output value when procedure has more than one output parameter");
247: }
248: return (this .outParameterNames.size() > 0 ? this .outParameterNames
249: .get(0)
250: : 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
267: .createMetaDataProvider(dataSource, this );
268: }
269:
270: /**
271: * Process the list of parameters provided and if procedure column metedata is used the
272: * parameters will be matched against the metadata information and any missing ones will
273: * be automatically included
274: * @param parameters the list of parameters ti use as a base
275: */
276: public void processParameters(List<SqlParameter> parameters) {
277: this .callParameters = reconcileParameters(parameters);
278: }
279:
280: /**
281: * Reconcile the provided parameters with available metadata and add new ones where appropriate
282: */
283: private List<SqlParameter> reconcileParameters(
284: List<SqlParameter> parameters) {
285: final List<SqlParameter> declaredReturnParameters = new ArrayList<SqlParameter>();
286:
287: final Map<String, SqlParameter> declaredParameters = new LinkedHashMap<String, SqlParameter>();
288:
289: boolean returnDeclared = false;
290:
291: List<String> outParameterNames = new ArrayList<String>();
292:
293: // separate implicit return parameters from explicit parameters
294: for (SqlParameter parameter : parameters) {
295: if (parameter.isResultsParameter()) {
296: declaredReturnParameters.add(parameter);
297: } else {
298: String parameterNameToMatch = metaDataProvider
299: .parameterNameToUse(parameter.getName())
300: .toLowerCase();
301: declaredParameters.put(parameterNameToMatch, parameter);
302: if (parameter instanceof SqlOutParameter) {
303: outParameterNames.add(parameter.getName());
304: if (this .isFunction()) {
305: if (!returnDeclared)
306: this .setFunctionReturnName(parameter
307: .getName());
308: returnDeclared = true;
309: }
310: }
311: }
312: }
313: this .setOutParameterNames(outParameterNames);
314:
315: final List<SqlParameter> workParameters = new ArrayList<SqlParameter>();
316: workParameters.addAll(declaredReturnParameters);
317:
318: if (!metaDataProvider.isProcedureColumnMetaDataUsed()) {
319: workParameters.addAll(declaredParameters.values());
320: return workParameters;
321: }
322:
323: Map limitedInParameterNamesMap = new HashMap(
324: limitedInParameterNames.size());
325: for (String limitedParameterName : limitedInParameterNames) {
326: limitedInParameterNamesMap.put(metaDataProvider
327: .parameterNameToUse(limitedParameterName)
328: .toLowerCase(), limitedParameterName);
329: }
330:
331: for (CallParameterMetaData meta : metaDataProvider
332: .getCallParameterMetaData()) {
333: String parNameToCheck = null;
334: if (meta.getParameterName() != null)
335: parNameToCheck = metaDataProvider.parameterNameToUse(
336: meta.getParameterName()).toLowerCase();
337: String parNameToUse = metaDataProvider
338: .parameterNameToUse(meta.getParameterName());
339: if (declaredParameters.containsKey(parNameToCheck)
340: || (meta.getParameterType() == DatabaseMetaData.procedureColumnReturn && returnDeclared)) {
341: SqlParameter parameter;
342: if (meta.getParameterType() == DatabaseMetaData.procedureColumnReturn) {
343: parameter = declaredParameters.get(this
344: .getFunctionReturnName());
345: if (parameter == null
346: && this .getOutParameterNames().size() > 0) {
347: parameter = declaredParameters.get(this
348: .getOutParameterNames().get(0)
349: .toLowerCase());
350: }
351: if (parameter == null) {
352: throw new InvalidDataAccessApiUsageException(
353: "Unable to locate declared parameter for function return value - "
354: + " add an SqlOutParameter with name \""
355: + this .getFunctionReturnName()
356: + "\"");
357: } else {
358: this .setFunctionReturnName(parameter.getName());
359: }
360: } else {
361: parameter = declaredParameters.get(parNameToCheck);
362: }
363: if (parameter != null) {
364: workParameters.add(parameter);
365: if (logger.isDebugEnabled()) {
366: logger.debug("Using declared parameter for: "
367: + (parNameToUse == null ? this
368: .getFunctionReturnName()
369: : parNameToUse));
370: }
371: }
372: } else {
373: if (meta.getParameterType() == DatabaseMetaData.procedureColumnReturn) {
374: if (!this .isFunction()
375: && !this .isReturnValueRequired()
376: && metaDataProvider
377: .byPassReturnParameter(meta
378: .getParameterName())) {
379: if (logger.isDebugEnabled()) {
380: logger
381: .debug("Bypassing metadata return parameter for: "
382: + meta.getParameterName());
383: }
384: } else {
385: String returnNameToUse = (meta
386: .getParameterName() == null || meta
387: .getParameterName().length() < 1) ? this
388: .getFunctionReturnName()
389: : parNameToUse;
390: workParameters.add(new SqlOutParameter(
391: returnNameToUse, meta.getSqlType()));
392: if (this .isFunction())
393: outParameterNames.add(returnNameToUse);
394: if (logger.isDebugEnabled()) {
395: logger
396: .debug("Added metadata return parameter for: "
397: + returnNameToUse);
398: }
399: }
400: } else {
401: if (meta.getParameterType() == DatabaseMetaData.procedureColumnOut
402: || meta.getParameterType() == DatabaseMetaData.procedureColumnInOut) {
403: workParameters.add(metaDataProvider
404: .createDefaultOutParameter(
405: parNameToUse, meta));
406: outParameterNames.add(parNameToUse);
407: if (logger.isDebugEnabled()) {
408: logger
409: .debug("Added metadata out parameter for: "
410: + parNameToUse);
411: }
412: } else {
413: if (limitedInParameterNames.size() == 0
414: || limitedInParameterNamesMap
415: .containsKey(parNameToUse
416: .toLowerCase())) {
417: workParameters.add(metaDataProvider
418: .createDefaultInParameter(
419: parNameToUse, meta));
420: if (logger.isDebugEnabled()) {
421: logger
422: .debug("Added metadata in parameter for: "
423: + parNameToUse);
424: }
425: } else {
426: if (logger.isDebugEnabled()) {
427: logger
428: .debug("Limited set of parameters "
429: + limitedInParameterNamesMap
430: .keySet()
431: + " skipped parameter for: "
432: + parNameToUse);
433: }
434: }
435: }
436: }
437: }
438: }
439:
440: return workParameters;
441:
442: }
443:
444: /**
445: * Match input parameter values with the parameters declared to be used in the call
446: * @param parameterSource the input values
447: * @return a Map containing the matched parameter names with the value taken from the input
448: */
449: //TODO provide a SqlParameterValue when sql type is specified
450: public Map<String, Object> matchInParameterValuesWithCallParameters(
451: SqlParameterSource parameterSource) {
452: Map<String, Object> matchedParameters = new HashMap<String, Object>(
453: callParameters.size());
454: for (SqlParameter parameter : callParameters) {
455: if (parameter.getName() != null) {
456: Object value = null;
457: boolean match = false;
458: if (parameterSource.hasValue(parameter.getName()
459: .toLowerCase())) {
460: value = parameterSource.getValue(parameter
461: .getName().toLowerCase());
462: match = true;
463: } else {
464: String propertyName = JdbcUtils
465: .convertUnderscoreNameToPropertyName(parameter
466: .getName());
467: if (parameterSource.hasValue(propertyName)) {
468: value = parameterSource.getValue(propertyName);
469: match = true;
470: }
471: }
472: if (match) {
473: matchedParameters.put(parameter.getName(), value);
474: }
475: }
476: }
477: return matchedParameters;
478: }
479:
480: /**
481: * Match input parameter values with the parameters declared to be used in the call
482: * @param inParameters the input values
483: * @return a Map containing the matched parameter names with the value taken from the input
484: */
485: public Map<String, Object> matchInParameterValuesWithCallParameters(
486: Map<String, Object> inParameters) {
487: if (!metaDataProvider.isProcedureColumnMetaDataUsed()) {
488: return inParameters;
489: }
490: Map<String, String> callParameterNames = new HashMap<String, String>(
491: callParameters.size());
492: for (SqlParameter parameter : callParameters) {
493: String parameterName = parameter.getName();
494: String parameterNameToMatch = metaDataProvider
495: .parameterNameToUse(parameterName);
496: if (parameterNameToMatch != null)
497: callParameterNames.put(parameterNameToMatch
498: .toLowerCase(), parameterName);
499: }
500: Map<String, Object> matchedParameters = new HashMap<String, Object>(
501: inParameters.size());
502: for (String parameterName : inParameters.keySet()) {
503: String parameterNameToMatch = metaDataProvider
504: .parameterNameToUse(parameterName);
505: String callParameterName = callParameterNames
506: .get(parameterNameToMatch.toLowerCase());
507: if (callParameterName == null) {
508: logger
509: .warn("Unable to locate the corresponding parameter for \""
510: + parameterName
511: + "\" in the parameters used: "
512: + callParameterNames.keySet());
513: } else {
514: matchedParameters.put(callParameterName, inParameters
515: .get(parameterName));
516: }
517:
518: }
519: if (logger.isDebugEnabled()) {
520: logger.debug("Matching " + inParameters + " with "
521: + callParameterNames);
522: }
523: return matchedParameters;
524: }
525:
526: /**
527: * Build the call string based on configuration and metadata information
528: * @return the call string to be used
529: */
530: public String createCallString() {
531: String callString;
532: int parameterCount = 0;
533: String catalogNameToUse = metaDataProvider
534: .catalogNameToUse(this .getCatalogName());
535: String schemaNameToUse = metaDataProvider.schemaNameToUse(this
536: .getSchemaName());
537: String procedureNameToUse = metaDataProvider
538: .procedureNameToUse(this .getProcedureName());
539: if (this .isFunction() || this .isReturnValueRequired()) {
540: callString = "{? = call "
541: + (catalogNameToUse != null
542: && catalogNameToUse.length() > 0 ? catalogNameToUse
543: + "."
544: : "")
545: + (schemaNameToUse != null
546: && schemaNameToUse.length() > 0 ? schemaNameToUse
547: + "."
548: : "") + procedureNameToUse + "(";
549: parameterCount = -1;
550: } else {
551: callString = "{call "
552: + (catalogNameToUse != null
553: && catalogNameToUse.length() > 0 ? catalogNameToUse
554: + "."
555: : "")
556: + (schemaNameToUse != null
557: && schemaNameToUse.length() > 0 ? schemaNameToUse
558: + "."
559: : "") + procedureNameToUse + "(";
560: }
561: for (SqlParameter parameter : callParameters) {
562: if (!(parameter.isResultsParameter())) {
563: if (parameterCount > 0) {
564: callString += ", ";
565: }
566: if (parameterCount >= 0) {
567: callString += "?";
568: }
569: parameterCount++;
570: }
571: }
572: callString += ")}";
573:
574: return callString;
575: }
576:
577: }
|