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}