View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.jdbc.core.metadata;
18  
19  import java.util.ArrayList;
20  import java.util.LinkedHashMap;
21  import java.util.LinkedHashSet;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.Set;
25  import javax.sql.DataSource;
26  
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  
30  import org.springframework.dao.InvalidDataAccessApiUsageException;
31  import org.springframework.jdbc.core.SqlTypeValue;
32  import org.springframework.jdbc.core.namedparam.SqlParameterSource;
33  import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
34  import org.springframework.jdbc.support.JdbcUtils;
35  import org.springframework.jdbc.support.nativejdbc.NativeJdbcExtractor;
36  
37  /**
38   * Class to manage context metadata used for the configuration
39   * and execution of operations on a database table.
40   *
41   * @author Thomas Risberg
42   * @author Juergen Hoeller
43   * @since 2.5
44   */
45  public class TableMetaDataContext {
46  
47  	/** Logger available to subclasses */
48  	protected final Log logger = LogFactory.getLog(getClass());
49  
50  	/** name of procedure to call **/
51  	private String tableName;
52  
53  	/** name of catalog for call **/
54  	private String catalogName;
55  
56  	/** name of schema for call **/
57  	private String schemaName;
58  
59  	/** List of columns objects to be used in this context */
60  	private List<String> tableColumns = new ArrayList<String>();
61  
62  	/** should we access insert parameter meta data info or not */
63  	private boolean accessTableColumnMetaData = true;
64  
65  	/** should we override default for including synonyms for meta data lookups */
66  	private boolean overrideIncludeSynonymsDefault = false;
67  
68  	/** the provider of table meta data */
69  	private TableMetaDataProvider metaDataProvider;
70  
71  	/** are we using generated key columns */
72  	private boolean generatedKeyColumnsUsed = false;
73  
74  	/** NativeJdbcExtractor to be used to retrieve the native connection */
75  	NativeJdbcExtractor nativeJdbcExtractor;
76  
77  
78  	/**
79  	 * Set the name of the table for this context.
80  	 */
81  	public void setTableName(String tableName) {
82  		this.tableName = tableName;
83  	}
84  
85  	/**
86  	 * Get the name of the table for this context.
87  	 */
88  	public String getTableName() {
89  		return this.tableName;
90  	}
91  
92  	/**
93  	 * Set the name of the catalog for this context.
94  	 */
95  	public void setCatalogName(String catalogName) {
96  		this.catalogName = catalogName;
97  	}
98  
99  	/**
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 	 * Does this database support the JDBC 3.0 feature of retrieving generated keys
158 	 * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}?
159 	 */
160 	public boolean isGetGeneratedKeysSupported() {
161 		return this.metaDataProvider.isGetGeneratedKeysSupported();
162 	}
163 
164 	/**
165 	 * Does this database support simple query to retrieve generated keys
166 	 * when the JDBC 3.0 feature is not supported.
167 	 * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}?
168 	 */
169 	public boolean isGetGeneratedKeysSimulated() {
170 		return this.metaDataProvider.isGetGeneratedKeysSimulated();
171 	}
172 
173 	/**
174 	 * Does this database support simple query to retrieve generated keys
175 	 * when the JDBC 3.0 feature is not supported.
176 	 * {@link java.sql.DatabaseMetaData#supportsGetGeneratedKeys()}?
177 	 */
178 	public String getSimulationQueryForGetGeneratedKey(String tableName, String keyColumnName) {
179 		return this.metaDataProvider.getSimpleQueryForGetGeneratedKey(tableName, keyColumnName);
180 	}
181 
182 	/**
183 	 * Is a column name String array for retrieving generated keys supported?
184 	 * {@link java.sql.Connection#createStruct(String, Object[])}?
185 	 */
186 	public boolean isGeneratedKeysColumnNameArraySupported() {
187 		return this.metaDataProvider.isGeneratedKeysColumnNameArraySupported();
188 	}
189 
190 	/**
191 	 * Set {@link NativeJdbcExtractor} to be used to retrieve the native connection.
192 	 */
193 	public void setNativeJdbcExtractor(NativeJdbcExtractor nativeJdbcExtractor) {
194 		this.nativeJdbcExtractor = nativeJdbcExtractor;
195 	}
196 
197 
198 	/**
199 	 * Process the current meta data with the provided configuration options.
200 	 * @param dataSource the DataSource being used
201 	 * @param declaredColumns any columns that are declared
202 	 * @param generatedKeyNames name of generated keys
203 	 */
204 	public void processMetaData(DataSource dataSource, List<String> declaredColumns, String[] generatedKeyNames) {
205 		this.metaDataProvider = TableMetaDataProviderFactory.createMetaDataProvider(dataSource, this, this.nativeJdbcExtractor);
206 		this.tableColumns = reconcileColumnsToUse(declaredColumns, generatedKeyNames);
207 	}
208 
209 	/**
210 	 * Compare columns created from metadata with declared columns and return a reconciled list.
211 	 * @param declaredColumns declared column names
212 	 * @param generatedKeyNames names of generated key columns
213 	 */
214 	protected List<String> reconcileColumnsToUse(List<String> declaredColumns, String[] generatedKeyNames) {
215 		if (generatedKeyNames.length > 0) {
216 			this.generatedKeyColumnsUsed = true;
217 		}
218 		if (declaredColumns.size() > 0) {
219 			return new ArrayList<String>(declaredColumns);
220 		}
221 		Set<String> keys = new LinkedHashSet<String>(generatedKeyNames.length);
222 		for (String key : generatedKeyNames) {
223 			keys.add(key.toUpperCase());
224 		}
225 		List<String> columns = new ArrayList<String>();
226 		for (TableParameterMetaData meta : metaDataProvider.getTableParameterMetaData()) {
227 			if (!keys.contains(meta.getParameterName().toUpperCase())) {
228 				columns.add(meta.getParameterName());
229 			}
230 		}
231 		return columns;
232 	}
233 
234 	/**
235 	 * Match the provided column names and values with the list of columns used.
236 	 * @param parameterSource the parameter names and values
237 	 */
238 	public List<Object> matchInParameterValuesWithInsertColumns(SqlParameterSource parameterSource) {
239 		List<Object> values = new ArrayList<Object>();
240 		// for parameter source lookups we need to provide caseinsensitive lookup support since the
241 		// database metadata is not necessarily providing case sensitive column names
242 		Map<String, String> caseInsensitiveParameterNames =
243 				SqlParameterSourceUtils.extractCaseInsensitiveParameterNames(parameterSource);
244 		for (String column : this.tableColumns) {
245 			if (parameterSource.hasValue(column)) {
246 				values.add(SqlParameterSourceUtils.getTypedValue(parameterSource, column));
247 			}
248 			else {
249 				String lowerCaseName = column.toLowerCase();
250 				if (parameterSource.hasValue(lowerCaseName)) {
251 					values.add(SqlParameterSourceUtils.getTypedValue(parameterSource, lowerCaseName));
252 				}
253 				else {
254 					String propertyName = JdbcUtils.convertUnderscoreNameToPropertyName(column);
255 					if (parameterSource.hasValue(propertyName)) {
256 						values.add(SqlParameterSourceUtils.getTypedValue(parameterSource, propertyName));
257 					}
258 					else {
259 						if (caseInsensitiveParameterNames.containsKey(lowerCaseName)) {
260 							values.add(
261 									SqlParameterSourceUtils.getTypedValue(parameterSource,
262 											caseInsensitiveParameterNames.get(lowerCaseName)));
263 						}
264 						else {
265 							values.add(null);
266 						}
267 					}
268 				}
269 			}
270 		}
271 		return values;
272 	}
273 
274 	/**
275 	 * Match the provided column names and values with the list of columns used.
276 	 * @param inParameters the parameter names and values
277 	 */
278 	public List<Object> matchInParameterValuesWithInsertColumns(Map<String, ?> inParameters) {
279 		List<Object> values = new ArrayList<Object>();
280 		Map<String, Object> source = new LinkedHashMap<String, Object>(inParameters.size());
281 		for (String key : inParameters.keySet()) {
282 			source.put(key.toLowerCase(), inParameters.get(key));
283 		}
284 		for (String column : this.tableColumns) {
285 			values.add(source.get(column.toLowerCase()));
286 		}
287 		return values;
288 	}
289 
290 
291 	/**
292 	 * Build the insert string based on configuration and metadata information
293 	 * @return the insert string to be used
294 	 */
295 	public String createInsertString(String... generatedKeyNames) {
296 		Set<String> keys = new LinkedHashSet<String>(generatedKeyNames.length);
297 		for (String key : generatedKeyNames) {
298 			keys.add(key.toUpperCase());
299 		}
300 		StringBuilder insertStatement = new StringBuilder();
301 		insertStatement.append("INSERT INTO ");
302 		if (this.getSchemaName() != null) {
303 			insertStatement.append(this.getSchemaName());
304 			insertStatement.append(".");
305 		}
306 		insertStatement.append(this.getTableName());
307 		insertStatement.append(" (");
308 		int columnCount = 0;
309 		for (String columnName : this.getTableColumns()) {
310 			if (!keys.contains(columnName.toUpperCase())) {
311 				columnCount++;
312 				if (columnCount > 1) {
313 					insertStatement.append(", ");
314 				}
315 				insertStatement.append(columnName);
316 			}
317 		}
318 		insertStatement.append(") VALUES(");
319 		if (columnCount < 1) {
320 			if (this.generatedKeyColumnsUsed) {
321 				logger.info("Unable to locate non-key columns for table '" +
322 						this.getTableName() + "' so an empty insert statement is generated");
323 			}
324 			else {
325 				throw new InvalidDataAccessApiUsageException("Unable to locate columns for table '" +
326 						this.getTableName() + "' so an insert statement can't be generated");
327 			}
328 		}
329 		for (int i = 0; i < columnCount; i++) {
330 			if (i > 0) {
331 				insertStatement.append(", ");
332 			}
333 			insertStatement.append("?");
334 		}
335 		insertStatement.append(")");
336 		return insertStatement.toString();
337 	}
338 
339 	/**
340 	 * Build the array of {@link java.sql.Types} based on configuration and metadata information
341 	 * @return the array of types to be used
342 	 */
343 	public int[] createInsertTypes() {
344 		int[] types = new int[this.getTableColumns().size()];
345 		List<TableParameterMetaData> parameters = this.metaDataProvider.getTableParameterMetaData();
346 		Map<String, TableParameterMetaData> parameterMap =
347 				new LinkedHashMap<String, TableParameterMetaData>(parameters.size());
348 		for (TableParameterMetaData tpmd : parameters) {
349 			parameterMap.put(tpmd.getParameterName().toUpperCase(), tpmd);
350 		}
351 		int typeIndx = 0;
352 		for (String column : this.getTableColumns()) {
353 			if (column == null) {
354 				types[typeIndx] = SqlTypeValue.TYPE_UNKNOWN;
355 			}
356 			else {
357 				TableParameterMetaData tpmd = parameterMap.get(column.toUpperCase());
358 				if (tpmd != null) {
359 					types[typeIndx] = tpmd.getSqlType();
360 				}
361 				else {
362 					types[typeIndx] = SqlTypeValue.TYPE_UNKNOWN;
363 				}
364 			}
365 			typeIndx++;
366 		}
367 		return types;
368 	}
369 
370 }