001/*
002 * Copyright 2015-2022 Transmogrify LLC, 2022-2025 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.pyranid;
018
019import javax.annotation.Nonnull;
020import javax.annotation.concurrent.NotThreadSafe;
021import java.sql.ResultSet;
022import java.sql.SQLException;
023import java.util.List;
024import java.util.Locale;
025import java.util.Optional;
026
027import static java.util.Objects.requireNonNull;
028
029/**
030 * Contract for mapping a {@link ResultSet} row to the specified type.
031 * <p>
032 * A production-ready concrete implementation is available via the following static methods:
033 * <ul>
034 *   <li>{@link #withDefaultConfiguration()}</li>
035 *   <li>{@link #withPlanCachingEnabled(Boolean)} (builder)</li>
036 *   <li>{@link #withCustomColumnMappers(List)} (builder)</li>
037 *   <li>{@link #withNormalizationLocale(Locale)} (builder)</li>
038 * </ul>
039 * <p>
040 * How to acquire an instance:
041 * <pre>{@code  // With out-of-the-box defaults
042 * ResultSetMapper default = ResultSetMapper.withDefaultConfiguration();
043 *
044 * // Customized
045 * ResultSetMapper custom = ResultSetMapper.withPlanCachingEnabled(false)
046 *  .customColumnMappers(List.of(...))
047 *  .normalizationLocale(Locale.forLanguageTag("pt-BR"))
048 *  .build();}</pre> Or, implement your own: <pre>{@code  ResultSetMapper myImpl = new ResultSetMapper() {
049 *   @Nonnull
050 *   @Override
051 *   public <T> Optional<T> map(
052 *     @Nonnull StatementContext<T> statementContext,
053 *     @Nonnull ResultSet resultSet,
054 *     @Nonnull Class<T> resultSetRowType,
055 *     @Nonnull InstanceProvider instanceProvider
056 *   ) throws SQLException {
057 *     // TODO: pull data from resultSet and apply to a new instance of T
058 *     return Optional.empty();
059 *   }
060 * };}</pre>
061 *
062 * @author <a href="https://www.revetkn.com">Mark Allen</a>
063 * @since 1.0.0
064 */
065@FunctionalInterface
066public interface ResultSetMapper {
067        /**
068         * Maps the current row of {@code resultSet} to the result class indicated by {@code statementContext}.
069         *
070         * @param <T>              result instance type token
071         * @param statementContext current SQL context
072         * @param resultSet        provides raw row data to pull from
073         * @param resultSetRowType the type to which the {@link ResultSet} row should be marshaled
074         * @param instanceProvider instance-creation factory, used to instantiate {@code resultSetRowType} row objects
075         * @return an {@link Optional} containing an instance of the given {@code resultClass}, or {@link Optional#empty()} to indicate a {@code null} value
076         * @throws SQLException if an error occurs during mapping
077         */
078        @Nonnull
079        <T> Optional<T> map(@Nonnull StatementContext<T> statementContext,
080                                                                                        @Nonnull ResultSet resultSet,
081                                                                                        @Nonnull Class<T> resultSetRowType,
082                                                                                        @Nonnull InstanceProvider instanceProvider) throws SQLException;
083
084        /**
085         * Acquires a builder for a concrete implementation of this interface, specifying the locale to use when massaging JDBC column names for matching against Java property names.
086         *
087         * @param normalizationLocale the locale to use when massaging JDBC column names for matching against Java property names
088         * @return a {@code Builder} for a concrete implementation
089         */
090        @Nonnull
091        static Builder withNormalizationLocale(@Nonnull Locale normalizationLocale) {
092                requireNonNull(normalizationLocale);
093
094                new ResultSetMapper() {
095                        @Nonnull
096                        @Override
097                        public <T> Optional<T> map(
098                                        @Nonnull StatementContext<T> statementContext,
099                                        @Nonnull ResultSet resultSet,
100                                        @Nonnull Class<T> resultSetRowType,
101                                        @Nonnull InstanceProvider instanceProvider) {
102                                return Optional.empty();
103                        }
104                };
105
106                return new Builder().normalizationLocale(normalizationLocale);
107        }
108
109        /**
110         * Acquires a builder for a concrete implementation of this interface, specifying a {@link List} of custom column-specific mapping logic to apply, in priority order.
111         *
112         * @param customColumnMappers a {@link List} of custom column-specific mapping logic to apply, in priority order
113         * @return a {@code Builder} for a concrete implementation
114         */
115        @Nonnull
116        static Builder withCustomColumnMappers(@Nonnull List<CustomColumnMapper> customColumnMappers) {
117                requireNonNull(customColumnMappers);
118                return new Builder().customColumnMappers(customColumnMappers);
119        }
120
121        /**
122         * Acquires a builder for a concrete implementation of this interface, specifying whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping.
123         *
124         * @param planCachingEnabled whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping
125         * @return a {@code Builder} for a concrete implementation
126         */
127        @Nonnull
128        static Builder withPlanCachingEnabled(@Nonnull Boolean planCachingEnabled) {
129                requireNonNull(planCachingEnabled);
130                return new Builder().planCachingEnabled(planCachingEnabled);
131        }
132
133        /**
134         * Acquires a concrete implementation of this interface with out-of-the-box defaults.
135         * <p>
136         * The returned instance is thread-safe.
137         *
138         * @return a concrete implementation of this interface with out-of-the-box defaults
139         */
140        @Nonnull
141        static ResultSetMapper withDefaultConfiguration() {
142                return new Builder().build();
143        }
144
145        /**
146         * Builder used to construct a standard implementation of {@link ResultSetMapper}.
147         * <p>
148         * This class is intended for use by a single thread.
149         *
150         * @author <a href="https://www.revetkn.com">Mark Allen</a>
151         * @since 3.0.0
152         */
153        @NotThreadSafe
154        class Builder {
155                @Nonnull
156                Locale normalizationLocale;
157                @Nonnull
158                List<CustomColumnMapper> customColumnMappers;
159                @Nonnull
160                Boolean planCachingEnabled;
161
162                private Builder() {
163                        this.normalizationLocale = Locale.getDefault();
164                        this.customColumnMappers = List.of();
165                        this.planCachingEnabled = true;
166                }
167
168                /**
169                 * Specifies the locale to use when massaging JDBC column names for matching against Java property names.
170                 *
171                 * @param normalizationLocale the locale to use when massaging JDBC column names for matching against Java property names
172                 * @return this {@code Builder}, for chaining
173                 */
174                @Nonnull
175                public Builder normalizationLocale(@Nonnull Locale normalizationLocale) {
176                        requireNonNull(normalizationLocale);
177                        this.normalizationLocale = normalizationLocale;
178                        return this;
179                }
180
181                /**
182                 * Specifies a {@link List} of custom column-specific mapping logic to apply, in priority order.
183                 *
184                 * @param customColumnMappers a {@link List} of custom column-specific mapping logic to apply, in priority order
185                 * @return this {@code Builder}, for chaining
186                 */
187                @Nonnull
188                public Builder customColumnMappers(@Nonnull List<CustomColumnMapper> customColumnMappers) {
189                        requireNonNull(customColumnMappers);
190                        this.customColumnMappers = customColumnMappers;
191                        return this;
192                }
193
194                /**
195                 * Specifies whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping.
196                 *
197                 * @param planCachingEnabled whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping
198                 * @return this {@code Builder}, for chaining
199                 */
200                @Nonnull
201                public Builder planCachingEnabled(@Nonnull Boolean planCachingEnabled) {
202                        requireNonNull(planCachingEnabled);
203                        this.planCachingEnabled = planCachingEnabled;
204                        return this;
205                }
206
207                /**
208                 * Constructs a default {@code ResultSetMapper} instance.
209                 * <p>
210                 * The constructed instance is thread-safe.
211                 *
212                 * @return a {@code ResultSetMapper} instance
213                 */
214                @Nonnull
215                public ResultSetMapper build() {
216                        return new DefaultResultSetMapper(this);
217                }
218        }
219}