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}