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}