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}