001package org.itsallcode.jdbc.resultset.generic;
002
003import java.sql.ResultSet;
004import java.sql.SQLException;
005import java.util.*;
006
007import org.itsallcode.jdbc.UncheckedSQLException;
008import org.itsallcode.jdbc.dialect.DbDialect;
009import org.itsallcode.jdbc.resultset.*;
010
011/**
012 * This {@link ContextRowMapper} converts a row to the generic {@link Row} type.
013 * 
014 * @param <T> generic row type
015 */
016public class GenericRowMapper<T> implements RowMapper<T> {
017    private ResultSetRowBuilder rowBuilder;
018    private final ColumnValuesConverter<T> converter;
019    private final DbDialect dialect;
020
021    /**
022     * Create a new instance.
023     * 
024     * @param dialect   database dialect
025     * @param converter optionally converts each generic {@link Row} to a different
026     *                  type.
027     */
028    public GenericRowMapper(final DbDialect dialect, final ColumnValuesConverter<T> converter) {
029        this.dialect = Objects.requireNonNull(dialect, "dialect");
030        this.converter = Objects.requireNonNull(converter, "converter");
031    }
032
033    @Override
034    public T mapRow(final ResultSet resultSet, final int rowNum) throws SQLException {
035        if (rowBuilder == null) {
036            rowBuilder = new ResultSetRowBuilder(SimpleMetaData.create(resultSet));
037        }
038        final Row row = rowBuilder.buildRow(ConvertingResultSet.create(dialect, resultSet), rowNum);
039        return converter.mapRow(row);
040    }
041
042    private static final class ResultSetRowBuilder {
043        private final SimpleMetaData metadata;
044
045        private ResultSetRowBuilder(final SimpleMetaData metaData) {
046            this.metadata = metaData;
047        }
048
049        private Row buildRow(final ResultSet resultSet, final int rowIndex) {
050            final List<ColumnMetaData> columns = metadata.columns();
051            final List<ColumnValue> fields = new ArrayList<>(columns.size());
052            for (final ColumnMetaData column : columns) {
053                final ColumnValue field = getField(resultSet, column, rowIndex);
054                fields.add(field);
055            }
056            return new Row(rowIndex, columns, fields);
057        }
058
059        private static ColumnValue getField(final ResultSet resultSet, final ColumnMetaData column,
060                final int rowIndex) {
061            final Object value = getValue(resultSet, column, rowIndex);
062            return new ColumnValue(column.type(), value);
063        }
064
065        private static Object getValue(final ResultSet resultSet, final ColumnMetaData column, final int rowIndex) {
066            try {
067                return resultSet.getObject(column.columnIndex());
068            } catch (final SQLException e) {
069                throw new UncheckedSQLException("Error extracting value for row " + rowIndex + " / column " + column,
070                        e);
071            }
072        }
073    }
074
075    /**
076     * A simplified row mapper that gets a list of column values as input.
077     * 
078     * @param <T> generic row type
079     */
080    @FunctionalInterface
081    @SuppressWarnings("java:S1711") // Explicit interface instead of generic Function<>
082    public interface ColumnValuesConverter<T> {
083        /**
084         * Convert a single row.
085         * 
086         * @param row column values
087         * @return converted object
088         */
089        T mapRow(Row row);
090    }
091}