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