001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.dbutils;
018
019import java.sql.ResultSet;
020import java.sql.ResultSetMetaData;
021import java.sql.SQLException;
022import java.util.HashMap;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.Locale;
026import java.util.Map;
027
028/**
029 * Basic implementation of the {@code RowProcessor} interface.
030 *
031 * <p>
032 * This class is thread-safe.
033 * </p>
034 *
035 * @see RowProcessor
036 */
037public class BasicRowProcessor implements RowProcessor {
038
039    /**
040     * The default BeanProcessor instance to use if not supplied in the
041     * constructor.
042     */
043    private static final BeanProcessor defaultConvert = new BeanProcessor();
044
045    /**
046     * The Singleton instance of this class.
047     */
048    private static final BasicRowProcessor instance = new BasicRowProcessor();
049
050    protected static Map<String, Object> createCaseInsensitiveHashMap(final int cols) {
051        return new CaseInsensitiveHashMap(cols);
052    }
053
054    /**
055     * Returns the Singleton instance of this class.
056     *
057     * @return The single instance of this class.
058     * @deprecated Create instances with the constructors instead.  This will
059     * be removed after DbUtils 1.1.
060     */
061    @Deprecated
062    public static BasicRowProcessor instance() {
063        return instance;
064    }
065
066    /**
067     * Use this to process beans.
068     */
069    private final BeanProcessor convert;
070
071    /**
072     * BasicRowProcessor constructor.  Bean processing defaults to a
073     * BeanProcessor instance.
074     */
075    public BasicRowProcessor() {
076        this(defaultConvert);
077    }
078
079    /**
080     * BasicRowProcessor constructor.
081     * @param convert The BeanProcessor to use when converting columns to
082     * bean properties.
083     * @since DbUtils 1.1
084     */
085    public BasicRowProcessor(final BeanProcessor convert) {
086        super();
087        this.convert = convert;
088    }
089
090    /**
091     * Convert a {@code ResultSet} row into an {@code Object[]}.
092     * This implementation copies column values into the array in the same
093     * order they're returned from the {@code ResultSet}.  Array elements
094     * will be set to {@code null} if the column was SQL NULL.
095     *
096     * @see org.apache.commons.dbutils.RowProcessor#toArray(java.sql.ResultSet)
097     * @param rs ResultSet that supplies the array data
098     * @throws SQLException if a database access error occurs
099     * @return the newly created array
100     */
101    @Override
102    public Object[] toArray(final ResultSet rs) throws SQLException {
103        final ResultSetMetaData meta = rs.getMetaData();
104        final int cols = meta.getColumnCount();
105        final Object[] result = new Object[cols];
106
107        for (int i = 0; i < cols; i++) {
108            result[i] = rs.getObject(i + 1);
109        }
110
111        return result;
112    }
113
114    /**
115     * Convert a {@code ResultSet} row into a JavaBean.  This
116     * implementation delegates to a BeanProcessor instance.
117     * @see org.apache.commons.dbutils.RowProcessor#toBean(java.sql.ResultSet, java.lang.Class)
118     * @see org.apache.commons.dbutils.BeanProcessor#toBean(java.sql.ResultSet, java.lang.Class)
119     * @param <T> The type of bean to create
120     * @param rs ResultSet that supplies the bean data
121     * @param type Class from which to create the bean instance
122     * @throws SQLException if a database access error occurs
123     * @return the newly created bean
124     */
125    @Override
126    public <T> T toBean(final ResultSet rs, final Class<? extends T> type) throws SQLException {
127        return this.convert.toBean(rs, type);
128    }
129
130    /**
131     * Convert a {@code ResultSet} into a {@code List} of JavaBeans.
132     * This implementation delegates to a BeanProcessor instance.
133     * @see org.apache.commons.dbutils.RowProcessor#toBeanList(java.sql.ResultSet, java.lang.Class)
134     * @see org.apache.commons.dbutils.BeanProcessor#toBeanList(java.sql.ResultSet, java.lang.Class)
135     * @param <T> The type of bean to create
136     * @param rs ResultSet that supplies the bean data
137     * @param type Class from which to create the bean instance
138     * @throws SQLException if a database access error occurs
139     * @return A {@code List} of beans with the given type in the order
140     * they were returned by the {@code ResultSet}.
141     */
142    @Override
143    public <T> List<T> toBeanList(final ResultSet rs, final Class<? extends T> type) throws SQLException {
144        return this.convert.toBeanList(rs, type);
145    }
146
147    /**
148     * Convert a {@code ResultSet} row into a {@code Map}.
149     *
150     * <p>
151     * This implementation returns a {@code Map} with case insensitive column names as keys. Calls to
152     * {@code map.get("COL")} and {@code map.get("col")} return the same value. Furthermore this implementation
153     * will return an ordered map, that preserves the ordering of the columns in the ResultSet, so that iterating over
154     * the entry set of the returned map will return the first column of the ResultSet, then the second and so forth.
155     * </p>
156     *
157     * @param rs ResultSet that supplies the map data
158     * @return the newly created Map
159     * @throws SQLException if a database access error occurs
160     * @see org.apache.commons.dbutils.RowProcessor#toMap(java.sql.ResultSet)
161     */
162    @Override
163    public Map<String, Object> toMap(final ResultSet rs) throws SQLException {
164        final ResultSetMetaData rsmd = rs.getMetaData();
165        final int cols = rsmd.getColumnCount();
166        final Map<String, Object> result = createCaseInsensitiveHashMap(cols);
167
168        for (int i = 1; i <= cols; i++) {
169            String columnName = rsmd.getColumnLabel(i);
170            if (null == columnName || 0 == columnName.length()) {
171              columnName = rsmd.getColumnName(i);
172            }
173            result.put(columnName, rs.getObject(i));
174        }
175
176        return result;
177    }
178
179
180    /**
181     * A Map that converts all keys to lowercase Strings for case insensitive
182     * lookups.  This is needed for the toMap() implementation because
183     * databases don't consistently handle the casing of column names.
184     *
185     * <p>The keys are stored as they are given [BUG #DBUTILS-34], so we maintain
186     * an internal mapping from lowercase keys to the real keys in order to
187     * achieve the case insensitive lookup.
188     *
189     * <p>Note: This implementation does not allow {@code null}
190     * for key, whereas {@link LinkedHashMap} does, because of the code:
191     * <pre>
192     * key.toString().toLowerCase()
193     * </pre>
194     */
195    private static final class CaseInsensitiveHashMap extends LinkedHashMap<String, Object> {
196
197        private CaseInsensitiveHashMap(final int initialCapacity) {
198            super(initialCapacity);
199        }
200
201        /**
202         * The internal mapping from lowercase keys to the real keys.
203         *
204         * <p>
205         * Any query operation using the key
206         * ({@link #get(Object)}, {@link #containsKey(Object)})
207         * is done in three steps:
208         * <ul>
209         * <li>convert the parameter key to lower case</li>
210         * <li>get the actual key that corresponds to the lower case key</li>
211         * <li>query the map with the actual key</li>
212         * </ul>
213         * </p>
214         */
215        private final Map<String, String> lowerCaseMap = new HashMap<>();
216
217        /**
218         * Required for serialization support.
219         *
220         * @see java.io.Serializable
221         */
222        private static final long serialVersionUID = -2848100435296897392L;
223
224        /** {@inheritDoc} */
225        @Override
226        public boolean containsKey(final Object key) {
227            final Object realKey = lowerCaseMap.get(key.toString().toLowerCase(Locale.ENGLISH));
228            return super.containsKey(realKey);
229            // Possible optimisation here:
230            // Since the lowerCaseMap contains a mapping for all the keys,
231            // we could just do this:
232            // return lowerCaseMap.containsKey(key.toString().toLowerCase());
233        }
234
235        /** {@inheritDoc} */
236        @Override
237        public Object get(final Object key) {
238            final Object realKey = lowerCaseMap.get(key.toString().toLowerCase(Locale.ENGLISH));
239            return super.get(realKey);
240        }
241
242        /** {@inheritDoc} */
243        @Override
244        public Object put(final String key, final Object value) {
245            /*
246             * In order to keep the map and lowerCaseMap synchronized,
247             * we have to remove the old mapping before putting the
248             * new one. Indeed, oldKey and key are not necessaliry equals.
249             * (That's why we call super.remove(oldKey) and not just
250             * super.put(key, value))
251             */
252            final Object oldKey = lowerCaseMap.put(key.toLowerCase(Locale.ENGLISH), key);
253            final Object oldValue = super.remove(oldKey);
254            super.put(key, value);
255            return oldValue;
256        }
257
258        /** {@inheritDoc} */
259        @Override
260        public void putAll(final Map<? extends String, ?> m) {
261            for (final Map.Entry<? extends String, ?> entry : m.entrySet()) {
262                final String key = entry.getKey();
263                final Object value = entry.getValue();
264                this.put(key, value);
265            }
266        }
267
268        /** {@inheritDoc} */
269        @Override
270        public Object remove(final Object key) {
271            final Object realKey = lowerCaseMap.remove(key.toString().toLowerCase(Locale.ENGLISH));
272            return super.remove(realKey);
273        }
274    }
275
276}