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        @NonNull
089        Integer DEFAULT_PLAN_CACHE_CAPACITY = 1024;
090
091        /**
092         * Retained for source and binary compatibility. Preferred custom column mappers are not cached.
093         *
094         * @deprecated this value has no effect and will be removed in 5.0.0
095         */
096        @Deprecated(since = "4.5.0", forRemoval = true)
097        int DEFAULT_PREFERRED_COLUMN_MAPPER_CACHE_CAPACITY = 256;
098
099        /**
100         * 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.
101         *
102         * @param normalizationLocale the locale to use when massaging JDBC column names for matching against Java property names
103         * @return a {@code Builder} for a concrete implementation
104         */
105        @NonNull
106        static Builder withNormalizationLocale(@NonNull Locale normalizationLocale) {
107                requireNonNull(normalizationLocale);
108                return new Builder().normalizationLocale(normalizationLocale);
109        }
110
111        /**
112         * 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.
113         *
114         * @param customColumnMappers a {@link List} of custom column-specific mapping logic to apply, in priority order
115         * @return a {@code Builder} for a concrete implementation
116         */
117        @NonNull
118        static Builder withCustomColumnMappers(@NonNull List<@NonNull CustomColumnMapper> customColumnMappers) {
119                requireNonNull(customColumnMappers);
120                return new Builder().customColumnMappers(customColumnMappers);
121        }
122
123        /**
124         * 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.
125         * <p>
126         * Disabling plan caching is primarily useful for highly dynamic schemas; the non-planned path allocates per-row maps and does more reflection.
127         *
128         * @param planCachingEnabled whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping
129         * @return a {@code Builder} for a concrete implementation
130         */
131        @NonNull
132        static Builder withPlanCachingEnabled(@NonNull Boolean planCachingEnabled) {
133                requireNonNull(planCachingEnabled);
134                return new Builder().planCachingEnabled(planCachingEnabled);
135        }
136
137        /**
138         * Acquires a concrete implementation of this interface with out-of-the-box defaults.
139         * <p>
140         * The returned instance is thread-safe.
141         *
142         * @return a concrete implementation of this interface with out-of-the-box defaults
143         */
144        @NonNull
145        static ResultSetMapper withDefaultConfiguration() {
146                return new Builder().build();
147        }
148
149        /**
150         * Builder used to construct a standard implementation of {@link ResultSetMapper}.
151         * <p>
152         * This class is intended for use by a single thread.
153         *
154         * @author <a href="https://www.revetkn.com">Mark Allen</a>
155         * @since 3.0.0
156         */
157        @NotThreadSafe
158        class Builder {
159                @NonNull
160                Locale normalizationLocale;
161                @NonNull
162                List<@NonNull CustomColumnMapper> customColumnMappers;
163                @NonNull
164                Boolean planCachingEnabled;
165                @NonNull
166                Integer planCacheCapacity;
167                @NonNull
168
169                private Builder() {
170                        this.normalizationLocale = Locale.ROOT;
171                        this.customColumnMappers = List.of();
172                        this.planCachingEnabled = true;
173                        this.planCacheCapacity = DEFAULT_PLAN_CACHE_CAPACITY;
174                }
175
176                /**
177                 * Specifies the locale to use when massaging JDBC column names for matching against Java property names.
178                 *
179                 * @param normalizationLocale the locale to use when massaging JDBC column names for matching against Java property names
180                 * @return this {@code Builder}, for chaining
181                 */
182                @NonNull
183                public Builder normalizationLocale(@NonNull Locale normalizationLocale) {
184                        requireNonNull(normalizationLocale);
185                        this.normalizationLocale = normalizationLocale;
186                        return this;
187                }
188
189                /**
190                 * Specifies a {@link List} of custom column-specific mapping logic to apply, in priority order.
191                 *
192                 * @param customColumnMappers a {@link List} of custom column-specific mapping logic to apply, in priority order
193                 * @return this {@code Builder}, for chaining
194                 */
195                @NonNull
196                public Builder customColumnMappers(@NonNull List<@NonNull CustomColumnMapper> customColumnMappers) {
197                        requireNonNull(customColumnMappers);
198                        this.customColumnMappers = customColumnMappers;
199                        return this;
200                }
201
202                /**
203                 * Specifies whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping.
204                 * <p>
205                 * Disabling plan caching is primarily useful for highly dynamic schemas; the non-planned path allocates per-row maps and does more reflection.
206                 *
207                 * @param planCachingEnabled whether an internal "mapping plan" cache should be used to speed up {@link ResultSet} mapping
208                 * @return this {@code Builder}, for chaining
209                 */
210                @NonNull
211                public Builder planCachingEnabled(@NonNull Boolean planCachingEnabled) {
212                        requireNonNull(planCachingEnabled);
213                        this.planCachingEnabled = planCachingEnabled;
214                        return this;
215                }
216
217                /**
218                 * Specifies the maximum number of row-mapping plans to cache per result class when plan caching is enabled.
219                 * <p>
220                 * Use {@code 0} for an unbounded cache.
221                 *
222                 * @param planCacheCapacity maximum number of cached plans per result class, or {@code 0} for unbounded.
223                 *                          Defaults to {@link #DEFAULT_PLAN_CACHE_CAPACITY}.
224                 * @return this {@code Builder}, for chaining
225                 */
226                @NonNull
227                public Builder planCacheCapacity(@NonNull Integer planCacheCapacity) {
228                        requireNonNull(planCacheCapacity);
229                        if (planCacheCapacity < 0)
230                                throw new IllegalArgumentException("Plan cache capacity must be >= 0");
231                        this.planCacheCapacity = planCacheCapacity;
232                        return this;
233                }
234
235                /**
236                 * Retained for source and binary compatibility. This setting has no effect.
237                 * <p>
238                 * Pyranid always evaluates applicable custom column mappers in their configured priority order. This method is
239                 * deprecated and will be removed in 5.0.0.
240                 *
241                 * @param preferredColumnMapperCacheCapacity ignored compatibility value; must be greater than or equal to {@code 0}
242                 * @return this {@code Builder}, for chaining
243                 */
244                @Deprecated(since = "4.5.0", forRemoval = true)
245                @NonNull
246                public Builder preferredColumnMapperCacheCapacity(@NonNull Integer preferredColumnMapperCacheCapacity) {
247                        requireNonNull(preferredColumnMapperCacheCapacity);
248                        if (preferredColumnMapperCacheCapacity < 0)
249                                throw new IllegalArgumentException("Preferred column mapper cache capacity must be >= 0");
250                        return this;
251                }
252
253                /**
254                 * Constructs a default {@code ResultSetMapper} instance.
255                 * <p>
256                 * The constructed instance is thread-safe.
257                 *
258                 * @return a {@code ResultSetMapper} instance
259                 */
260                @NonNull
261                public ResultSetMapper build() {
262                        return new DefaultResultSetMapper(this);
263                }
264        }
265}