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