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