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