View Javadoc
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    *      http://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 }