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;
020import org.jspecify.annotations.Nullable;
021
022import javax.annotation.concurrent.NotThreadSafe;
023import java.sql.ResultSet;
024import java.sql.SQLException;
025import java.util.List;
026import java.util.Locale;
027import java.util.Optional;
028
029import static java.util.Objects.requireNonNull;
030
031/**
032 * Contract for mapping a {@link ResultSet} row to the specified type.
033 * <p>
034 * A production-ready concrete implementation is available via the following static methods:
035 * <ul>
036 *   <li>{@link #withDefaultConfiguration()}</li>
037 *   <li>{@link #withPlanCachingEnabled(Boolean)} (builder)</li>
038 *   <li>{@link #withCustomColumnMappers(List)} (builder)</li>
039 *   <li>{@link #withNormalizationLocale(Locale)} (builder)</li>
040 * </ul>
041 * <p>
042 * How to acquire an instance:
043 * <pre>{@code  // With out-of-the-box defaults
044 * ResultSetMapper default = ResultSetMapper.withDefaultConfiguration();
045 *
046 * // Customized
047 * ResultSetMapper custom = ResultSetMapper.withPlanCachingEnabled(false)
048 *  .customColumnMappers(List.of(...))
049 *  .normalizationLocale(Locale.forLanguageTag("pt-BR"))
050 *  .build();}</pre> Or, implement your own: <pre>{@code  ResultSetMapper myImpl = new ResultSetMapper() {
051 *   @NonNull
052 *   @Override
053 *   public <T> Optional<T> map(
054 *     @NonNull StatementContext<T> statementContext,
055 *     @NonNull ResultSet resultSet,
056 *     @NonNull Class<T> resultSetRowType,
057 *     @NonNull InstanceProvider instanceProvider
058 *   ) throws SQLException {
059 *     // Pull data from resultSet and apply to a new instance of T
060 *     return Optional.empty();
061 *   }
062 * };}</pre>
063 *
064 * @author <a href="https://www.revetkn.com">Mark Allen</a>
065 * @since 1.0.0
066 */
067@FunctionalInterface
068public interface ResultSetMapper {
069        /**
070         * Maps the current row of {@code resultSet} to the result class indicated by {@code statementContext}.
071         *
072         * @param <T>              result instance type token
073         * @param statementContext current SQL context
074         * @param resultSet        provides raw row data to pull from
075         * @param resultSetRowType the type to which the {@link ResultSet} row should be marshaled
076         * @param instanceProvider instance-creation factory, used to instantiate {@code resultSetRowType} row objects
077         * @return an {@link Optional} containing an instance of the given {@code resultClass}, or {@link Optional#empty()} to indicate a {@code null} value
078         * @throws SQLException if an error occurs during mapping
079         */
080        @NonNull
081        <T> Optional<T> map(@NonNull StatementContext<T> statementContext,
082                                                                                        @NonNull ResultSet resultSet,
083                                                                                        @NonNull Class<T> resultSetRowType,
084                                                                                        @NonNull InstanceProvider instanceProvider) throws SQLException;
085
086        /**
087         * Default maximum number of cached row-mapping plans per result class.
088         */
089        @NonNull
090        Integer DEFAULT_PLAN_CACHE_CAPACITY = 1024;
091
092        /**
093         * Retained for source and binary compatibility. Preferred custom column mappers are not cached.
094         *
095         * @deprecated this value has no effect and will be removed in 5.0.0
096         */
097        @Deprecated(since = "4.5.0", forRemoval = true)
098        int DEFAULT_PREFERRED_COLUMN_MAPPER_CACHE_CAPACITY = 256;
099
100        /**
101         * 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.
102         *
103         * @param normalizationLocale the locale to use when massaging JDBC column names for matching against Java property names
104         * @return a {@code Builder} for a concrete implementation
105         */
106        @NonNull
107        static Builder withNormalizationLocale(@NonNull Locale normalizationLocale) {
108                requireNonNull(normalizationLocale);
109                return new Builder().normalizationLocale(normalizationLocale);
110        }
111
112        /**
113         * 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.
114         *
115         * @param customColumnMappers a {@link List} of custom column-specific mapping logic to apply, in priority order
116         * @return a {@code Builder} for a concrete implementation
117         */
118        @NonNull
119        static Builder withCustomColumnMappers(@NonNull List<@NonNull CustomColumnMapper> customColumnMappers) {
120                requireNonNull(customColumnMappers);
121                return new Builder().customColumnMappers(customColumnMappers);
122        }
123
124        /**
125         * 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.
126         * <p>
127         * Disabling plan caching is primarily useful for highly dynamic schemas; plans use the same mapping rules but are compiled without retention.
128         *
129         * @param planCachingEnabled whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping
130         * @return a {@code Builder} for a concrete implementation
131         */
132        @NonNull
133        static Builder withPlanCachingEnabled(@NonNull Boolean planCachingEnabled) {
134                requireNonNull(planCachingEnabled);
135                return new Builder().planCachingEnabled(planCachingEnabled);
136        }
137
138        /**
139         * Acquires a concrete implementation of this interface with out-of-the-box defaults.
140         * <p>
141         * The returned instance is thread-safe.
142         *
143         * @return a concrete implementation of this interface with out-of-the-box defaults
144         */
145        @NonNull
146        static ResultSetMapper withDefaultConfiguration() {
147                return new Builder().build();
148        }
149
150        /**
151         * Builder used to construct a standard implementation of {@link ResultSetMapper}.
152         * <p>
153         * This class is intended for use by a single thread.
154         *
155         * @author <a href="https://www.revetkn.com">Mark Allen</a>
156         * @since 3.0.0
157         */
158        @NotThreadSafe
159        class Builder {
160                @NonNull
161                Locale normalizationLocale;
162                @NonNull
163                List<@NonNull CustomColumnMapper> customColumnMappers;
164                @NonNull
165                Boolean planCachingEnabled;
166                @NonNull
167                Integer planCacheCapacity;
168                @Nullable
169                Long maxLobBytes = 16L * 1024 * 1024;
170                @Nullable
171                Long maxLobCharacters = 8L * 1024 * 1024;
172
173                private Builder() {
174                        this.normalizationLocale = Locale.ROOT;
175                        this.customColumnMappers = List.of();
176                        this.planCachingEnabled = true;
177                        this.planCacheCapacity = DEFAULT_PLAN_CACHE_CAPACITY;
178                }
179
180                /**
181                 * Specifies the locale to use when massaging JDBC column names for matching against Java property names.
182                 *
183                 * @param normalizationLocale the locale to use when massaging JDBC column names for matching against Java property names
184                 * @return this {@code Builder}, for chaining
185                 */
186                @NonNull
187                public Builder normalizationLocale(@NonNull Locale normalizationLocale) {
188                        requireNonNull(normalizationLocale);
189                        this.normalizationLocale = normalizationLocale;
190                        return this;
191                }
192
193                /**
194                 * Specifies a {@link List} of custom column-specific mapping logic to apply, in priority order.
195                 *
196                 * @param customColumnMappers a {@link List} of custom column-specific mapping logic to apply, in priority order
197                 * @return this {@code Builder}, for chaining
198                 */
199                @NonNull
200                public Builder customColumnMappers(@NonNull List<@NonNull CustomColumnMapper> customColumnMappers) {
201                        requireNonNull(customColumnMappers);
202                        this.customColumnMappers = customColumnMappers;
203                        return this;
204                }
205
206                /**
207                 * Specifies whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping.
208                 * <p>
209                 * Disabling plan caching is primarily useful for highly dynamic schemas; plans use the same mapping rules but are compiled without retention.
210                 *
211                 * @param planCachingEnabled whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping
212                 * @return this {@code Builder}, for chaining
213                 */
214                @NonNull
215                public Builder planCachingEnabled(@NonNull Boolean planCachingEnabled) {
216                        requireNonNull(planCachingEnabled);
217                        this.planCachingEnabled = planCachingEnabled;
218                        return this;
219                }
220
221                /**
222                 * Specifies the maximum number of row-mapping plans to cache per result class when plan caching is enabled.
223                 * <p>
224                 * Use {@code 0} for an unbounded cache.
225                 *
226                 * @param planCacheCapacity maximum number of cached plans per result class, or {@code 0} for unbounded.
227                 *                          Defaults to {@link #DEFAULT_PLAN_CACHE_CAPACITY}.
228                 * @return this {@code Builder}, for chaining
229                 */
230                @NonNull
231                public Builder planCacheCapacity(@NonNull Integer planCacheCapacity) {
232                        requireNonNull(planCacheCapacity);
233                        if (planCacheCapacity < 0)
234                                throw new IllegalArgumentException("Plan cache capacity must be >= 0");
235                        this.planCacheCapacity = planCacheCapacity;
236                        return this;
237                }
238
239                /**
240                 * Limits each BLOB value materialized by the default mapper as {@code byte[]}.
241                 * Defaults to 16 MiB. The limit does not cover driver buffering, custom mappers,
242                 * ordinary binary columns, or the aggregate size of a result list.
243                 * This setter enables the binary limit even after {@link #unboundedLobMaterialization()}.
244                 *
245                 * @param maxLobBytes strictly positive maximum byte count
246                 * @return this builder
247                 * @since 4.8.0
248                 */
249                @NonNull
250                public Builder maxLobBytes(@NonNull Long maxLobBytes) {
251                        requireNonNull(maxLobBytes);
252                        if (maxLobBytes <= 0)
253                                throw new IllegalArgumentException("Maximum LOB bytes must be > 0");
254                        this.maxLobBytes = maxLobBytes;
255                        return this;
256                }
257
258                /**
259                 * Limits each CLOB/NCLOB value materialized by the default mapper as {@link String}.
260                 * Defaults to 8,388,608 UTF-16 code units. The limit does not cover driver buffering,
261                 * custom mappers, ordinary character columns, or the aggregate size of a result list.
262                 * This setter enables the character limit even after {@link #unboundedLobMaterialization()}.
263                 *
264                 * @param maxLobCharacters strictly positive maximum UTF-16 code-unit count
265                 * @return this builder
266                 * @since 4.8.0
267                 */
268                @NonNull
269                public Builder maxLobCharacters(@NonNull Long maxLobCharacters) {
270                        requireNonNull(maxLobCharacters);
271                        if (maxLobCharacters <= 0)
272                                throw new IllegalArgumentException("Maximum LOB characters must be > 0");
273                        this.maxLobCharacters = maxLobCharacters;
274                        return this;
275                }
276
277                /**
278                 * Disables both configurable LOB limits. Java array and string size limits still apply.
279                 * Later calls to either finite-limit setter re-enable that limit independently.
280                 *
281                 * @return this builder
282                 * @since 4.8.0
283                 */
284                @NonNull
285                public Builder unboundedLobMaterialization() {
286                        this.maxLobBytes = null;
287                        this.maxLobCharacters = null;
288                        return this;
289                }
290
291                /**
292                 * Retained for source and binary compatibility. This setting has no effect.
293                 * <p>
294                 * Pyranid always evaluates applicable custom column mappers in their configured priority order. This method is
295                 * deprecated and will be removed in 5.0.0.
296                 *
297                 * @param preferredColumnMapperCacheCapacity ignored compatibility value; must be greater than or equal to {@code 0}
298                 * @return this {@code Builder}, for chaining
299                 */
300                @Deprecated(since = "4.5.0", forRemoval = true)
301                @NonNull
302                public Builder preferredColumnMapperCacheCapacity(@NonNull Integer preferredColumnMapperCacheCapacity) {
303                        requireNonNull(preferredColumnMapperCacheCapacity);
304                        if (preferredColumnMapperCacheCapacity < 0)
305                                throw new IllegalArgumentException("Preferred column mapper cache capacity must be >= 0");
306                        return this;
307                }
308
309                /**
310                 * Constructs a default {@code ResultSetMapper} instance.
311                 * <p>
312                 * The constructed instance is thread-safe.
313                 *
314                 * @return a {@code ResultSetMapper} instance
315                 */
316                @NonNull
317                public ResultSetMapper build() {
318                        return new DefaultResultSetMapper(this);
319                }
320        }
321}