001/*
002 * Copyright 2002-2020 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;
018
019import java.sql.Connection;
020import java.sql.PreparedStatement;
021import java.sql.ResultSet;
022import java.sql.SQLException;
023import java.sql.Types;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashSet;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Set;
031
032import org.springframework.dao.InvalidDataAccessApiUsageException;
033import org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor;
034
035/**
036 * Helper class that efficiently creates multiple {@link PreparedStatementCreator}
037 * objects with different parameters based on a SQL statement and a single
038 * set of parameter declarations.
039 *
040 * @author Rod Johnson
041 * @author Thomas Risberg
042 * @author Juergen Hoeller
043 */
044public class PreparedStatementCreatorFactory {
045
046        /** The SQL, which won't change when the parameters change */
047        private final String sql;
048
049        /** List of SqlParameter objects (may not be {@code null}) */
050        private final List<SqlParameter> declaredParameters;
051
052        private int resultSetType = ResultSet.TYPE_FORWARD_ONLY;
053
054        private boolean updatableResults = false;
055
056        private boolean returnGeneratedKeys = false;
057
058        private String[] generatedKeysColumnNames;
059
060        private NativeJdbcExtractor nativeJdbcExtractor;
061
062
063        /**
064         * Create a new factory. Will need to add parameters via the
065         * {@link #addParameter} method or have no parameters.
066         * @param sql the SQL statement to execute
067         */
068        public PreparedStatementCreatorFactory(String sql) {
069                this.sql = sql;
070                this.declaredParameters = new LinkedList<SqlParameter>();
071        }
072
073        /**
074         * Create a new factory with the given SQL and JDBC types.
075         * @param sql the SQL statement to execute
076         * @param types int array of JDBC types
077         */
078        public PreparedStatementCreatorFactory(String sql, int... types) {
079                this.sql = sql;
080                this.declaredParameters = SqlParameter.sqlTypesToAnonymousParameterList(types);
081        }
082
083        /**
084         * Create a new factory with the given SQL and parameters.
085         * @param sql the SQL statement to execute
086         * @param declaredParameters list of {@link SqlParameter} objects
087         */
088        public PreparedStatementCreatorFactory(String sql, List<SqlParameter> declaredParameters) {
089                this.sql = sql;
090                this.declaredParameters = declaredParameters;
091        }
092
093
094        /**
095         * Add a new declared parameter.
096         * <p>Order of parameter addition is significant.
097         * @param param the parameter to add to the list of declared parameters
098         */
099        public void addParameter(SqlParameter param) {
100                this.declaredParameters.add(param);
101        }
102
103        /**
104         * Set whether to use prepared statements that return a specific type of ResultSet.
105         * @param resultSetType the ResultSet type
106         * @see java.sql.ResultSet#TYPE_FORWARD_ONLY
107         * @see java.sql.ResultSet#TYPE_SCROLL_INSENSITIVE
108         * @see java.sql.ResultSet#TYPE_SCROLL_SENSITIVE
109         */
110        public void setResultSetType(int resultSetType) {
111                this.resultSetType = resultSetType;
112        }
113
114        /**
115         * Set whether to use prepared statements capable of returning updatable ResultSets.
116         */
117        public void setUpdatableResults(boolean updatableResults) {
118                this.updatableResults = updatableResults;
119        }
120
121        /**
122         * Set whether prepared statements should be capable of returning auto-generated keys.
123         */
124        public void setReturnGeneratedKeys(boolean returnGeneratedKeys) {
125                this.returnGeneratedKeys = returnGeneratedKeys;
126        }
127
128        /**
129         * Set the column names of the auto-generated keys.
130         */
131        public void setGeneratedKeysColumnNames(String... names) {
132                this.generatedKeysColumnNames = names;
133        }
134
135        /**
136         * Specify the NativeJdbcExtractor to use for unwrapping PreparedStatements, if any.
137         */
138        public void setNativeJdbcExtractor(NativeJdbcExtractor nativeJdbcExtractor) {
139                this.nativeJdbcExtractor = nativeJdbcExtractor;
140        }
141
142
143        /**
144         * Return a new PreparedStatementSetter for the given parameters.
145         * @param params list of parameters (may be {@code null})
146         */
147        public PreparedStatementSetter newPreparedStatementSetter(List<?> params) {
148                return new PreparedStatementCreatorImpl(params != null ? params : Collections.emptyList());
149        }
150
151        /**
152         * Return a new PreparedStatementSetter for the given parameters.
153         * @param params the parameter array (may be {@code null})
154         */
155        public PreparedStatementSetter newPreparedStatementSetter(Object[] params) {
156                return new PreparedStatementCreatorImpl(params != null ? Arrays.asList(params) : Collections.emptyList());
157        }
158
159        /**
160         * Return a new PreparedStatementCreator for the given parameters.
161         * @param params list of parameters (may be {@code null})
162         */
163        public PreparedStatementCreator newPreparedStatementCreator(List<?> params) {
164                return new PreparedStatementCreatorImpl(params != null ? params : Collections.emptyList());
165        }
166
167        /**
168         * Return a new PreparedStatementCreator for the given parameters.
169         * @param params the parameter array (may be {@code null})
170         */
171        public PreparedStatementCreator newPreparedStatementCreator(Object[] params) {
172                return new PreparedStatementCreatorImpl(params != null ? Arrays.asList(params) : Collections.emptyList());
173        }
174
175        /**
176         * Return a new PreparedStatementCreator for the given parameters.
177         * @param sqlToUse the actual SQL statement to use (if different from
178         * the factory's, for example because of named parameter expanding)
179         * @param params the parameter array (may be {@code null})
180         */
181        public PreparedStatementCreator newPreparedStatementCreator(String sqlToUse, Object[] params) {
182                return new PreparedStatementCreatorImpl(
183                                sqlToUse, params != null ? Arrays.asList(params) : Collections.emptyList());
184        }
185
186
187        /**
188         * PreparedStatementCreator implementation returned by this class.
189         */
190        private class PreparedStatementCreatorImpl
191                        implements PreparedStatementCreator, PreparedStatementSetter, SqlProvider, ParameterDisposer {
192
193                private final String actualSql;
194
195                private final List<?> parameters;
196
197                public PreparedStatementCreatorImpl(List<?> parameters) {
198                        this(sql, parameters);
199                }
200
201                public PreparedStatementCreatorImpl(String actualSql, List<?> parameters) {
202                        this.actualSql = actualSql;
203                        this.parameters = parameters;
204                        if (parameters.size() != declaredParameters.size()) {
205                                // Account for named parameters being used multiple times
206                                Set<String> names = new HashSet<String>();
207                                for (int i = 0; i < parameters.size(); i++) {
208                                        Object param = parameters.get(i);
209                                        if (param instanceof SqlParameterValue) {
210                                                names.add(((SqlParameterValue) param).getName());
211                                        }
212                                        else {
213                                                names.add("Parameter #" + i);
214                                        }
215                                }
216                                if (names.size() != declaredParameters.size()) {
217                                        throw new InvalidDataAccessApiUsageException(
218                                                        "SQL [" + sql + "]: given " + names.size() +
219                                                        " parameters but expected " + declaredParameters.size());
220                                }
221                        }
222                }
223
224                @Override
225                public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
226                        PreparedStatement ps;
227                        if (generatedKeysColumnNames != null || returnGeneratedKeys) {
228                                if (generatedKeysColumnNames != null) {
229                                        ps = con.prepareStatement(this.actualSql, generatedKeysColumnNames);
230                                }
231                                else {
232                                        ps = con.prepareStatement(this.actualSql, PreparedStatement.RETURN_GENERATED_KEYS);
233                                }
234                        }
235                        else if (resultSetType == ResultSet.TYPE_FORWARD_ONLY && !updatableResults) {
236                                ps = con.prepareStatement(this.actualSql);
237                        }
238                        else {
239                                ps = con.prepareStatement(this.actualSql, resultSetType,
240                                        updatableResults ? ResultSet.CONCUR_UPDATABLE : ResultSet.CONCUR_READ_ONLY);
241                        }
242                        setValues(ps);
243                        return ps;
244                }
245
246                @Override
247                public void setValues(PreparedStatement ps) throws SQLException {
248                        // Determine PreparedStatement to pass to custom types.
249                        PreparedStatement psToUse = ps;
250                        if (nativeJdbcExtractor != null) {
251                                psToUse = nativeJdbcExtractor.getNativePreparedStatement(ps);
252                        }
253
254                        // Set arguments: Does nothing if there are no parameters.
255                        int sqlColIndx = 1;
256                        for (int i = 0; i < this.parameters.size(); i++) {
257                                Object in = this.parameters.get(i);
258                                SqlParameter declaredParameter;
259                                // SqlParameterValue overrides declared parameter meta-data, in particular for
260                                // independence from the declared parameter position in case of named parameters.
261                                if (in instanceof SqlParameterValue) {
262                                        SqlParameterValue paramValue = (SqlParameterValue) in;
263                                        in = paramValue.getValue();
264                                        declaredParameter = paramValue;
265                                }
266                                else {
267                                        if (declaredParameters.size() <= i) {
268                                                throw new InvalidDataAccessApiUsageException(
269                                                                "SQL [" + sql + "]: unable to access parameter number " + (i + 1) +
270                                                                " given only " + declaredParameters.size() + " parameters");
271
272                                        }
273                                        declaredParameter = declaredParameters.get(i);
274                                }
275                                if (in instanceof Collection && declaredParameter.getSqlType() != Types.ARRAY) {
276                                        Collection<?> entries = (Collection<?>) in;
277                                        for (Object entry : entries) {
278                                                if (entry instanceof Object[]) {
279                                                        Object[] valueArray = ((Object[]) entry);
280                                                        for (Object argValue : valueArray) {
281                                                                StatementCreatorUtils.setParameterValue(psToUse, sqlColIndx++, declaredParameter, argValue);
282                                                        }
283                                                }
284                                                else {
285                                                        StatementCreatorUtils.setParameterValue(psToUse, sqlColIndx++, declaredParameter, entry);
286                                                }
287                                        }
288                                }
289                                else {
290                                        StatementCreatorUtils.setParameterValue(psToUse, sqlColIndx++, declaredParameter, in);
291                                }
292                        }
293                }
294
295                @Override
296                public String getSql() {
297                        return sql;
298                }
299
300                @Override
301                public void cleanupParameters() {
302                        StatementCreatorUtils.cleanupParameters(this.parameters);
303                }
304
305                @Override
306                public String toString() {
307                        return "PreparedStatementCreator: sql=[" + sql + "]; parameters=" + this.parameters;
308                }
309        }
310
311}