1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.beanutils2.sql;
19
20 import java.sql.Date;
21 import java.sql.ResultSet;
22 import java.sql.ResultSetMetaData;
23 import java.sql.SQLException;
24 import java.sql.Time;
25 import java.sql.Timestamp;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Objects;
31
32 import org.apache.commons.beanutils2.DynaBean;
33 import org.apache.commons.beanutils2.DynaClass;
34 import org.apache.commons.beanutils2.DynaProperty;
35
36 /**
37 * <p>
38 * Provides common logic for JDBC implementations of {@link DynaClass}.
39 * </p>
40 */
41 abstract class AbstractJdbcDynaClass implements DynaClass {
42
43 /**
44 * Cross Reference for column name --> dyna property name (needed when lowerCase option is true)
45 */
46 private Map<String, String> columnNameXref;
47
48 /**
49 * <p>
50 * Flag defining whether column names should be lower cased when converted to property names.
51 * </p>
52 */
53 protected boolean lowerCase = true;
54
55 /**
56 * <p>
57 * The set of dynamic properties that are part of this {@link DynaClass}.
58 * </p>
59 */
60 protected DynaProperty[] properties;
61
62 /**
63 * <p>
64 * The set of dynamic properties that are part of this {@link DynaClass}, keyed by the property name. Individual descriptor instances will be the same
65 * instances as those in the {@code properties} list.
66 * </p>
67 */
68 protected Map<String, DynaProperty> propertiesMap = new HashMap<>();
69
70 /**
71 * <p>
72 * Flag defining whether column names or labels should be used.
73 */
74 private boolean useColumnLabel;
75
76 /**
77 * <p>
78 * Factory method to create a new DynaProperty for the given index into the result set metadata.
79 * </p>
80 *
81 * @param metadata is the result set metadata
82 * @param i is the column index in the metadata
83 * @return the newly created DynaProperty instance
84 * @throws SQLException If an error occurs accessing the SQL metadata
85 */
86 protected DynaProperty createDynaProperty(final ResultSetMetaData metadata, final int i) throws SQLException {
87 String columnName = null;
88 if (useColumnLabel) {
89 columnName = metadata.getColumnLabel(i);
90 }
91 if (columnName == null || columnName.trim().isEmpty()) {
92 columnName = metadata.getColumnName(i);
93 }
94 final String name = lowerCase ? columnName.toLowerCase() : columnName;
95 if (!name.equals(columnName)) {
96 if (columnNameXref == null) {
97 columnNameXref = new HashMap<>();
98 }
99 columnNameXref.put(name, columnName);
100 }
101 String className = null;
102 try {
103 final int sqlType = metadata.getColumnType(i);
104 switch (sqlType) {
105 case java.sql.Types.DATE:
106 return new DynaProperty(name, java.sql.Date.class);
107 case java.sql.Types.TIMESTAMP:
108 return new DynaProperty(name, java.sql.Timestamp.class);
109 case java.sql.Types.TIME:
110 return new DynaProperty(name, java.sql.Time.class);
111 default:
112 className = metadata.getColumnClassName(i);
113 }
114 } catch (final SQLException e) {
115 // this is a patch for HsqlDb to ignore exceptions
116 // thrown by its metadata implementation
117 }
118
119 // Default to Object type if no class name could be retrieved
120 // from the metadata
121 Class<?> clazz = Object.class;
122 if (className != null) {
123 clazz = loadClass(className);
124 }
125 return new DynaProperty(name, clazz);
126 }
127
128 /**
129 * Gets the table column name for the specified property name.
130 *
131 * @param name The property name
132 * @return The column name (which can be different if the <em>lowerCase</em> option is used).
133 */
134 protected String getColumnName(final String name) {
135 if (columnNameXref != null && columnNameXref.containsKey(name)) {
136 return columnNameXref.get(name);
137 }
138 return name;
139 }
140
141 /**
142 * <p>
143 * Gets an array of {@code PropertyDescriptor} for the properties currently defined in this DynaClass. If no properties are defined, a zero-length array
144 * will be returned.
145 * </p>
146 */
147 @Override
148 public DynaProperty[] getDynaProperties() {
149 return properties;
150 }
151
152 /**
153 * <p>
154 * Gets a property descriptor for the specified property, if it exists; otherwise, return {@code null}.
155 * </p>
156 *
157 * @param name Name of the dynamic property for which a descriptor is requested
158 * @throws IllegalArgumentException if no property name is specified
159 */
160 @Override
161 public DynaProperty getDynaProperty(final String name) {
162 return propertiesMap.get(Objects.requireNonNull(name, "name"));
163
164 }
165
166 /**
167 * <p>
168 * Gets the name of this DynaClass (analogous to the {@code getName()</code> method of <code>java.lang.Class}, which allows the same {@code DynaClass}
169 * implementation class to support different dynamic classes, with different sets of properties.
170 * </p>
171 */
172 @Override
173 public String getName() {
174 return this.getClass().getName();
175
176 }
177
178 /**
179 * Gets a column value from a {@link ResultSet} for the specified name.
180 *
181 * @param resultSet The result set
182 * @param name The property name
183 * @return The value
184 * @throws SQLException if an error occurs
185 */
186 protected Object getObject(final ResultSet resultSet, final String name) throws SQLException {
187 final DynaProperty property = getDynaProperty(name);
188 if (property == null) {
189 throw new IllegalArgumentException("Invalid name '" + name + "'");
190 }
191 final String columnName = getColumnName(name);
192 final Class<?> type = property.getType();
193
194 // java.sql.Date
195 if (type.equals(Date.class)) {
196 return resultSet.getDate(columnName);
197 }
198
199 // java.sql.Timestamp
200 if (type.equals(Timestamp.class)) {
201 return resultSet.getTimestamp(columnName);
202 }
203
204 // java.sql.Time
205 if (type.equals(Time.class)) {
206 return resultSet.getTime(columnName);
207 }
208
209 return resultSet.getObject(columnName);
210 }
211
212 /**
213 * <p>
214 * Introspect the metadata associated with our result set, and populate the {@code properties</code> and <code>propertiesMap} instance variables.
215 * </p>
216 *
217 * @param resultSet The {@code resultSet} whose metadata is to be introspected
218 * @throws SQLException if an error is encountered processing the result set metadata
219 */
220 protected void introspect(final ResultSet resultSet) throws SQLException {
221 // Accumulate an ordered list of DynaProperties
222 final List<DynaProperty> list = new ArrayList<>();
223 final ResultSetMetaData metadata = resultSet.getMetaData();
224 final int n = metadata.getColumnCount();
225 for (int i = 1; i <= n; i++) { // JDBC is one-relative!
226 final DynaProperty dynaProperty = createDynaProperty(metadata, i);
227 if (dynaProperty != null) {
228 list.add(dynaProperty);
229 }
230 }
231
232 // Convert this list into the internal data structures we need
233 properties = list.toArray(DynaProperty.EMPTY_ARRAY);
234 for (final DynaProperty property : properties) {
235 propertiesMap.put(property.getName(), property);
236 }
237 }
238
239 /**
240 * <p>
241 * Loads and returns the {@code Class} of the given name. By default, a load from the thread context class loader is attempted. If there is no such class
242 * loader, the class loader used to load this class will be utilized.
243 * </p>
244 *
245 * @param className The name of the class to load
246 * @return The loaded class
247 * @throws SQLException if an exception was thrown trying to load the specified class
248 */
249 protected Class<?> loadClass(final String className) throws SQLException {
250 try {
251 ClassLoader cl = Thread.currentThread().getContextClassLoader();
252 if (cl == null) {
253 cl = this.getClass().getClassLoader();
254 }
255 // use Class.forName() - see BEANUTILS-327
256 return Class.forName(className, false, cl);
257 } catch (final Exception e) {
258 throw new SQLException("Cannot load column class '" + className + "': " + e);
259 }
260 }
261
262 /**
263 * <p>
264 * Instantiate and return a new DynaBean instance, associated with this DynaClass. <strong>NOTE</strong> - This operation is not supported, and throws an
265 * exception.
266 * </p>
267 *
268 * @throws IllegalAccessException if the Class or the appropriate constructor is not accessible
269 * @throws InstantiationException if this Class represents an abstract class, an array class, a primitive type, or void; or if instantiation fails for some
270 * other reason
271 */
272 @Override
273 public DynaBean newInstance() throws IllegalAccessException, InstantiationException {
274 throw new UnsupportedOperationException("newInstance() not supported");
275 }
276
277 /**
278 * Sets whether the column label or name should be used for the property name.
279 *
280 * @param useColumnLabel true if the column label should be used, otherwise false
281 */
282 public void setUseColumnLabel(final boolean useColumnLabel) {
283 this.useColumnLabel = useColumnLabel;
284 }
285
286 }