001/*
002 * Copyright 2015-2022 Transmogrify LLC, 2022-2026 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 org.jspecify.annotations.NonNull;
020
021import javax.annotation.concurrent.NotThreadSafe;
022import java.sql.ResultSet;
023import java.sql.SQLException;
024import java.util.List;
025import java.util.Locale;
026import java.util.Optional;
027
028import static java.util.Objects.requireNonNull;
029
030/**
031 * Contract for mapping a {@link ResultSet} row to the specified type.
032 * <p>
033 * A production-ready concrete implementation is available via the following static methods:
034 * <ul>
035 *   <li>{@link #withDefaultConfiguration()}</li>
036 *   <li>{@link #withPlanCachingEnabled(Boolean)} (builder)</li>
037 *   <li>{@link #withCustomColumnMappers(List)} (builder)</li>
038 *   <li>{@link #withNormalizationLocale(Locale)} (builder)</li>
039 * </ul>
040 * <p>
041 * How to acquire an instance:
042 * <pre>{@code  // With out-of-the-box defaults
043 * ResultSetMapper default = ResultSetMapper.withDefaultConfiguration();
044 *
045 * // Customized
046 * ResultSetMapper custom = ResultSetMapper.withPlanCachingEnabled(false)
047 *  .customColumnMappers(List.of(...))
048 *  .normalizationLocale(Locale.forLanguageTag("pt-BR"))
049 *  .build();}</pre> Or, implement your own: <pre>{@code  ResultSetMapper myImpl = new ResultSetMapper() {
050 *   @NonNull
051 *   @Override
052 *   public <T> Optional<T> map(
053 *     @NonNull StatementContext<T> statementContext,
054 *     @NonNull ResultSet resultSet,
055 *     @NonNull Class<T> resultSetRowType,
056 *     @NonNull InstanceProvider instanceProvider
057 *   ) throws SQLException {
058 *     // Pull data from resultSet and apply to a new instance of T
059 *     return Optional.empty();
060 *   }
061 * };}</pre>
062 *
063 * @author <a href="https://www.revetkn.com">Mark Allen</a>
064 * @since 1.0.0
065 */
066@FunctionalInterface
067public interface ResultSetMapper {
068        /**
069         * Maps the current row of {@code resultSet} to the result class indicated by {@code statementContext}.
070         *
071         * @param <T>              result instance type token
072         * @param statementContext current SQL context
073         * @param resultSet        provides raw row data to pull from
074         * @param resultSetRowType the type to which the {@link ResultSet} row should be marshaled
075         * @param instanceProvider instance-creation factory, used to instantiate {@code resultSetRowType} row objects
076         * @return an {@link Optional} containing an instance of the given {@code resultClass}, or {@link Optional#empty()} to indicate a {@code null} value
077         * @throws SQLException if an error occurs during mapping
078         */
079        @NonNull
080        <T> Optional<T> map(@NonNull StatementContext<T> statementContext,
081                                                                                        @NonNull ResultSet resultSet,
082                                                                                        @NonNull Class<T> resultSetRowType,
083                                                                                        @NonNull InstanceProvider instanceProvider) throws SQLException;
084
085        /**
086         * Default maximum number of cached row-mapping plans per result class.
087         */
088        int DEFAULT_PLAN_CACHE_CAPACITY = 1024;
089
090        /**
091         * Default maximum number of cached preferred custom column mappers per source class.
092         */
093        int DEFAULT_PREFERRED_COLUMN_MAPPER_CACHE_CAPACITY = 256;
094
095        /**
096         * 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.
097         *
098         * @param normalizationLocale the locale to use when massaging JDBC column names for matching against Java property names
099         * @return a {@code Builder} for a concrete implementation
100         */
101        @NonNull
102        static Builder withNormalizationLocale(@NonNull Locale normalizationLocale) {
103                requireNonNull(normalizationLocale);
104
105                new ResultSetMapper() {
106                        @NonNull
107                        @Override
108                        public <T> Optional<T> map(
109                                        @NonNull StatementContext<T> statementContext,
110                                        @NonNull ResultSet resultSet,
111                                        @NonNull Class<T> resultSetRowType,
112                                        @NonNull InstanceProvider instanceProvider) {
113                                return Optional.empty();
114                        }
115                };
116
117                return new Builder().normalizationLocale(normalizationLocale);
118        }
119
120        /**
121         * 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.
122         *
123         * @param customColumnMappers a {@link List} of custom column-specific mapping logic to apply, in priority order
124         * @return a {@code Builder} for a concrete implementation
125         */
126        @NonNull
127        static Builder withCustomColumnMappers(@NonNull List<@NonNull CustomColumnMapper> customColumnMappers) {
128                requireNonNull(customColumnMappers);
129                return new Builder().customColumnMappers(customColumnMappers);
130        }
131
132        /**
133         * 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.
134         * <p>
135         * Disabling plan caching is primarily useful for highly dynamic schemas; the non-planned path allocates per-row maps and does more reflection.
136         *
137         * @param planCachingEnabled whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping
138         * @return a {@code Builder} for a concrete implementation
139         */
140        @NonNull
141        static Builder withPlanCachingEnabled(@NonNull Boolean planCachingEnabled) {
142                requireNonNull(planCachingEnabled);
143                return new Builder().planCachingEnabled(planCachingEnabled);
144        }
145
146        /**
147         * Acquires a concrete implementation of this interface with out-of-the-box defaults.
148         * <p>
149         * The returned instance is thread-safe.
150         *
151         * @return a concrete implementation of this interface with out-of-the-box defaults
152         */
153        @NonNull
154        static ResultSetMapper withDefaultConfiguration() {
155                return new Builder().build();
156        }
157
158        /**
159         * Builder used to construct a standard implementation of {@link ResultSetMapper}.
160         * <p>
161         * This class is intended for use by a single thread.
162         *
163         * @author <a href="https://www.revetkn.com">Mark Allen</a>
164         * @since 3.0.0
165         */
166        @NotThreadSafe
167        class Builder {
168                @NonNull
169                Locale normalizationLocale;
170                @NonNull
171                List<@NonNull CustomColumnMapper> customColumnMappers;
172                @NonNull
173                Boolean planCachingEnabled;
174                @NonNull
175                Integer planCacheCapacity;
176                @NonNull
177                Integer preferredColumnMapperCacheCapacity;
178
179                private Builder() {
180                        this.normalizationLocale = Locale.ROOT;
181                        this.customColumnMappers = List.of();
182                        this.planCachingEnabled = true;
183                        this.planCacheCapacity = DEFAULT_PLAN_CACHE_CAPACITY;
184                        this.preferredColumnMapperCacheCapacity = DEFAULT_PREFERRED_COLUMN_MAPPER_CACHE_CAPACITY;
185                }
186
187                /**
188                 * Specifies the locale to use when massaging JDBC column names for matching against Java property names.
189                 *
190                 * @param normalizationLocale the locale to use when massaging JDBC column names for matching against Java property names
191                 * @return this {@code Builder}, for chaining
192                 */
193                @NonNull
194                public Builder normalizationLocale(@NonNull Locale normalizationLocale) {
195                        requireNonNull(normalizationLocale);
196                        this.normalizationLocale = normalizationLocale;
197                        return this;
198                }
199
200                /**
201                 * Specifies a {@link List} of custom column-specific mapping logic to apply, in priority order.
202                 *
203                 * @param customColumnMappers a {@link List} of custom column-specific mapping logic to apply, in priority order
204                 * @return this {@code Builder}, for chaining
205                 */
206                @NonNull
207                public Builder customColumnMappers(@NonNull List<@NonNull CustomColumnMapper> customColumnMappers) {
208                        requireNonNull(customColumnMappers);
209                        this.customColumnMappers = customColumnMappers;
210                        return this;
211                }
212
213                /**
214                 * Specifies whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping.
215                 * <p>
216                 * Disabling plan caching is primarily useful for highly dynamic schemas; the non-planned path allocates per-row maps and does more reflection.
217                 *
218                 * @param planCachingEnabled whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping
219                 * @return this {@code Builder}, for chaining
220                 */
221                @NonNull
222                public Builder planCachingEnabled(@NonNull Boolean planCachingEnabled) {
223                        requireNonNull(planCachingEnabled);
224                        this.planCachingEnabled = planCachingEnabled;
225                        return this;
226                }
227
228                /**
229                 * Specifies the maximum number of row-mapping plans to cache per result class when plan caching is enabled.
230                 * <p>
231                 * Use {@code 0} for an unbounded cache.
232                 *
233                 * @param planCacheCapacity maximum number of cached plans per result class, or {@code 0} for unbounded.
234                 *                          Defaults to {@link #DEFAULT_PLAN_CACHE_CAPACITY}.
235                 * @return this {@code Builder}, for chaining
236                 */
237                @NonNull
238                public Builder planCacheCapacity(@NonNull Integer planCacheCapacity) {
239                        requireNonNull(planCacheCapacity);
240                        if (planCacheCapacity < 0)
241                                throw new IllegalArgumentException("Plan cache capacity must be >= 0");
242                        this.planCacheCapacity = planCacheCapacity;
243                        return this;
244                }
245
246                /**
247                 * Specifies the maximum number of cached preferred custom column mappers per source class.
248                 * <p>
249                 * Use {@code 0} for an unbounded cache.
250                 *
251                 * @param preferredColumnMapperCacheCapacity maximum number of cached preferred custom column mappers per source class,
252                 *                                           or {@code 0} for unbounded.
253                 *                                           Defaults to {@link #DEFAULT_PREFERRED_COLUMN_MAPPER_CACHE_CAPACITY}.
254                 * @return this {@code Builder}, for chaining
255                 */
256                @NonNull
257                public Builder preferredColumnMapperCacheCapacity(@NonNull Integer preferredColumnMapperCacheCapacity) {
258                        requireNonNull(preferredColumnMapperCacheCapacity);
259                        if (preferredColumnMapperCacheCapacity < 0)
260                                throw new IllegalArgumentException("Preferred column mapper cache capacity must be >= 0");
261                        this.preferredColumnMapperCacheCapacity = preferredColumnMapperCacheCapacity;
262                        return this;
263                }
264
265                /**
266                 * Constructs a default {@code ResultSetMapper} instance.
267                 * <p>
268                 * The constructed instance is thread-safe.
269                 *
270                 * @return a {@code ResultSetMapper} instance
271                 */
272                @NonNull
273                public ResultSetMapper build() {
274                        return new DefaultResultSetMapper(this);
275                }
276        }
277}