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}