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.metadata;
018
019import java.util.ArrayList;
020import java.util.LinkedHashMap;
021import java.util.LinkedHashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
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.SqlTypeValue;
032import org.springframework.jdbc.core.namedparam.SqlParameterSource;
033import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
034import org.springframework.jdbc.support.JdbcUtils;
035import org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor;
036
037/**
038 * Class to manage context meta-data used for the configuration
039 * and execution of operations on a database table.
040 *
041 * @author Thomas Risberg
042 * @author Juergen Hoeller
043 * @since 2.5
044 */
045public class TableMetaDataContext {
046
047        // Logger available to subclasses
048        protected final Log logger = LogFactory.getLog(getClass());
049
050        // Name of table for this context
051        private String tableName;
052
053        // Name of catalog for this context
054        private String catalogName;
055
056        // Name of schema for this context
057        private String schemaName;
058
059        // List of columns objects to be used in this context
060        private List<String> tableColumns = new ArrayList<String>();
061
062        // Should we access insert parameter meta-data info or not
063        private boolean accessTableColumnMetaData = true;
064
065        // Should we override default for including synonyms for meta-data lookups
066        private boolean overrideIncludeSynonymsDefault = false;
067
068        // The provider of table meta-data
069        private TableMetaDataProvider metaDataProvider;
070
071        // Are we using generated key columns
072        private boolean generatedKeyColumnsUsed = false;
073
074        // NativeJdbcExtractor to be used to retrieve the native connection
075        NativeJdbcExtractor nativeJdbcExtractor;
076
077
078        /**
079         * Set the name of the table for this context.
080         */
081        public void setTableName(String tableName) {
082                this.tableName = tableName;
083        }
084
085        /**
086         * Get the name of the table for this context.
087         */
088        public String getTableName() {
089                return this.tableName;
090        }
091
092        /**
093         * Set the name of the catalog for this context.
094         */
095        public void setCatalogName(String catalogName) {
096                this.catalogName = catalogName;
097        }
098
099        /**
100         * Get the name of the catalog for this context.
101         */
102        public String getCatalogName() {
103                return this.catalogName;
104        }
105
106        /**
107         * Set the name of the schema for this context.
108         */
109        public void setSchemaName(String schemaName) {
110                this.schemaName = schemaName;
111        }
112
113        /**
114         * Get the name of the schema for this context.
115         */
116        public String getSchemaName() {
117                return this.schemaName;
118        }
119
120        /**
121         * Specify whether we should access table column meta-data.
122         */
123        public void setAccessTableColumnMetaData(boolean accessTableColumnMetaData) {
124                this.accessTableColumnMetaData = accessTableColumnMetaData;
125        }
126
127        /**
128         * Are we accessing table meta-data?
129         */
130        public boolean isAccessTableColumnMetaData() {
131                return this.accessTableColumnMetaData;
132        }
133
134
135        /**
136         * Specify whether we should override default for accessing synonyms.
137         */
138        public void setOverrideIncludeSynonymsDefault(boolean override) {
139                this.overrideIncludeSynonymsDefault = override;
140        }
141
142        /**
143         * Are we overriding include synonyms default?
144         */
145        public boolean isOverrideIncludeSynonymsDefault() {
146                return this.overrideIncludeSynonymsDefault;
147        }
148
149        /**
150         * Get a List of the table column names.
151         */
152        public List<String> getTableColumns() {
153                return this.tableColumns;
154        }
155
156        /**
157         * Set {@link NativeJdbcExtractor} to be used to retrieve the native connection.
158         */
159        public void setNativeJdbcExtractor(NativeJdbcExtractor nativeJdbcExtractor) {
160                this.nativeJdbcExtractor = nativeJdbcExtractor;
161        }
162
163
164        /**
165         * Process the current meta-data with the provided configuration options.
166         * @param dataSource the DataSource being used
167         * @param declaredColumns any columns that are declared
168         * @param generatedKeyNames name of generated keys
169         */
170        public void processMetaData(DataSource dataSource, List<String> declaredColumns, String[] generatedKeyNames) {
171                this.metaDataProvider =
172                                TableMetaDataProviderFactory.createMetaDataProvider(dataSource, this, this.nativeJdbcExtractor);
173                this.tableColumns = reconcileColumnsToUse(declaredColumns, generatedKeyNames);
174        }
175
176        /**
177         * Compare columns created from meta-data with declared columns and return a reconciled list.
178         * @param declaredColumns declared column names
179         * @param generatedKeyNames names of generated key columns
180         */
181        protected List<String> reconcileColumnsToUse(List<String> declaredColumns, String[] generatedKeyNames) {
182                if (generatedKeyNames.length > 0) {
183                        this.generatedKeyColumnsUsed = true;
184                }
185                if (!declaredColumns.isEmpty()) {
186                        return new ArrayList<String>(declaredColumns);
187                }
188                Set<String> keys = new LinkedHashSet<String>(generatedKeyNames.length);
189                for (String key : generatedKeyNames) {
190                        keys.add(key.toUpperCase());
191                }
192                List<String> columns = new ArrayList<String>();
193                for (TableParameterMetaData meta : this.metaDataProvider.getTableParameterMetaData()) {
194                        if (!keys.contains(meta.getParameterName().toUpperCase())) {
195                                columns.add(meta.getParameterName());
196                        }
197                }
198                return columns;
199        }
200
201        /**
202         * Match the provided column names and values with the list of columns used.
203         * @param parameterSource the parameter names and values
204         */
205        public List<Object> matchInParameterValuesWithInsertColumns(SqlParameterSource parameterSource) {
206                List<Object> values = new ArrayList<Object>();
207                // For parameter source lookups we need to provide case-insensitive lookup support since the
208                // database meta-data is not necessarily providing case-sensitive column names
209                Map<String, String> caseInsensitiveParameterNames =
210                                SqlParameterSourceUtils.extractCaseInsensitiveParameterNames(parameterSource);
211                for (String column : this.tableColumns) {
212                        if (parameterSource.hasValue(column)) {
213                                values.add(SqlParameterSourceUtils.getTypedValue(parameterSource, column));
214                        }
215                        else {
216                                String lowerCaseName = column.toLowerCase();
217                                if (parameterSource.hasValue(lowerCaseName)) {
218                                        values.add(SqlParameterSourceUtils.getTypedValue(parameterSource, lowerCaseName));
219                                }
220                                else {
221                                        String propertyName = JdbcUtils.convertUnderscoreNameToPropertyName(column);
222                                        if (parameterSource.hasValue(propertyName)) {
223                                                values.add(SqlParameterSourceUtils.getTypedValue(parameterSource, propertyName));
224                                        }
225                                        else {
226                                                if (caseInsensitiveParameterNames.containsKey(lowerCaseName)) {
227                                                        values.add(SqlParameterSourceUtils.getTypedValue(
228                                                                        parameterSource, caseInsensitiveParameterNames.get(lowerCaseName)));
229                                                }
230                                                else {
231                                                        values.add(null);
232                                                }
233                                        }
234                                }
235                        }
236                }
237                return values;
238        }
239
240        /**
241         * Match the provided column names and values with the list of columns used.
242         * @param inParameters the parameter names and values
243         */
244        public List<Object> matchInParameterValuesWithInsertColumns(Map<String, ?> inParameters) {
245                List<Object> values = new ArrayList<Object>();
246                Map<String, Object> source = new LinkedHashMap<String, Object>(inParameters.size());
247                for (String key : inParameters.keySet()) {
248                        source.put(key.toLowerCase(), inParameters.get(key));
249                }
250                for (String column : this.tableColumns) {
251                        values.add(source.get(column.toLowerCase()));
252                }
253                return values;
254        }
255
256
257        /**
258         * Build the insert string based on configuration and meta-data information.
259         * @return the insert string to be used
260         */
261        public String createInsertString(String... generatedKeyNames) {
262                Set<String> keys = new LinkedHashSet<String>(generatedKeyNames.length);
263                for (String key : generatedKeyNames) {
264                        keys.add(key.toUpperCase());
265                }
266                StringBuilder insertStatement = new StringBuilder();
267                insertStatement.append("INSERT INTO ");
268                if (getSchemaName() != null) {
269                        insertStatement.append(getSchemaName());
270                        insertStatement.append(".");
271                }
272                insertStatement.append(getTableName());
273                insertStatement.append(" (");
274                int columnCount = 0;
275                for (String columnName : getTableColumns()) {
276                        if (!keys.contains(columnName.toUpperCase())) {
277                                columnCount++;
278                                if (columnCount > 1) {
279                                        insertStatement.append(", ");
280                                }
281                                insertStatement.append(columnName);
282                        }
283                }
284                insertStatement.append(") VALUES(");
285                if (columnCount < 1) {
286                        if (this.generatedKeyColumnsUsed) {
287                                if (logger.isInfoEnabled()) {
288                                        logger.info("Unable to locate non-key columns for table '" +
289                                                        getTableName() + "' so an empty insert statement is generated");
290                                }
291                        }
292                        else {
293                                throw new InvalidDataAccessApiUsageException("Unable to locate columns for table '" +
294                                                getTableName() + "' so an insert statement can't be generated");
295                        }
296                }
297                for (int i = 0; i < columnCount; i++) {
298                        if (i > 0) {
299                                insertStatement.append(", ");
300                        }
301                        insertStatement.append("?");
302                }
303                insertStatement.append(")");
304                return insertStatement.toString();
305        }
306
307        /**
308         * Build the array of {@link java.sql.Types} based on configuration and meta-data information.
309         * @return the array of types to be used
310         */
311        public int[] createInsertTypes() {
312                int[] types = new int[getTableColumns().size()];
313                List<TableParameterMetaData> parameters = this.metaDataProvider.getTableParameterMetaData();
314                Map<String, TableParameterMetaData> parameterMap =
315                                new LinkedHashMap<String, TableParameterMetaData>(parameters.size());
316                for (TableParameterMetaData tpmd : parameters) {
317                        parameterMap.put(tpmd.getParameterName().toUpperCase(), tpmd);
318                }
319                int typeIndx = 0;
320                for (String column : getTableColumns()) {
321                        if (column == null) {
322                                types[typeIndx] = SqlTypeValue.TYPE_UNKNOWN;
323                        }
324                        else {
325                                TableParameterMetaData tpmd = parameterMap.get(column.toUpperCase());
326                                if (tpmd != null) {
327                                        types[typeIndx] = tpmd.getSqlType();
328                                }
329                                else {
330                                        types[typeIndx] = SqlTypeValue.TYPE_UNKNOWN;
331                                }
332                        }
333                        typeIndx++;
334                }
335                return types;
336        }
337
338
339        /**
340         * Does this database support the JDBC 3.0 feature of retrieving generated keys:
341         * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}?
342         */
343        public boolean isGetGeneratedKeysSupported() {
344                return this.metaDataProvider.isGetGeneratedKeysSupported();
345        }
346
347        /**
348         * Does this database support simple query to retrieve generated keys
349         * when the JDBC 3.0 feature is not supported:
350         * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}?
351         */
352        public boolean isGetGeneratedKeysSimulated() {
353                return this.metaDataProvider.isGetGeneratedKeysSimulated();
354        }
355
356        /**
357         * Does this database support a simple query to retrieve generated keys
358         * when the JDBC 3.0 feature is not supported:
359         * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}?
360         * @deprecated as of 4.3.15, in favor of {@link #getSimpleQueryForGetGeneratedKey}
361         */
362        @Deprecated
363        public String getSimulationQueryForGetGeneratedKey(String tableName, String keyColumnName) {
364                return getSimpleQueryForGetGeneratedKey(tableName, keyColumnName);
365        }
366
367        /**
368         * Does this database support a simple query to retrieve generated keys
369         * when the JDBC 3.0 feature is not supported:
370         * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}?
371         */
372        public String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) {
373                return this.metaDataProvider.getSimpleQueryForGetGeneratedKey(tableName, keyColumnName);
374        }
375
376        /**
377         * Is a column name String array for retrieving generated keys supported?
378         * {@link java.sql.Connection#createStruct(String, Object[])}?
379         */
380        public boolean isGeneratedKeysColumnNameArraySupported() {
381                return this.metaDataProvider.isGeneratedKeysColumnNameArraySupported();
382        }
383
384}