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}