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.ThreadSafe; 023import java.sql.ResultSet; 024import java.sql.SQLException; 025import java.util.Optional; 026 027/** 028 * Enables per-column {@link ResultSet} mapping customization via {@link ResultSetMapper}. 029 * <p> 030 * When mapping JavaBean or {@link Record} rows, custom column mappers are applied to matched properties 031 * or components. Row-type custom mapping is intended for single-column results that represent the target value itself. 032 * 033 * @author <a href="https://www.revetkn.com">Mark Allen</a> 034 * @since 3.0.0 035 */ 036public interface CustomColumnMapper { 037 /** 038 * Perform custom mapping of a {@link ResultSet} column: given a {@code resultSetValue}, optionally return an instance of {@code targetType} instead. 039 * <p> 040 * This method is only invoked when {@code resultSetValue} is non-null. 041 * <p> 042 * If this method throws {@link SQLException}, the default {@link ResultSetMapper} wraps it in {@link DatabaseException} 043 * so row-plan caching settings do not affect the exception type observed by callers. 044 * 045 * @param statementContext current SQL context 046 * @param resultSet the {@link ResultSet} from which data was read 047 * @param resultSetValue the already-read value from the {@link ResultSet}, to be optionally converted to an instance of {@code targetType} 048 * @param targetType the type to which the {@code resultSetValue} should be converted 049 * @param columnIndex 1-based column index 050 * @param columnLabel normalized column label, if available 051 * @param instanceProvider instance-creation factory, may be used to instantiate values 052 * @return the result of the custom column mapping operation - either {@link MappingResult#of(Object)} to indicate a successfully-mapped value or {@link MappingResult#fallback()} if Pyranid should fall back to the registered {@link ResultSetMapper} mapping behavior. 053 * @throws SQLException if an error occurs during mapping 054 */ 055 @NonNull 056 MappingResult map(@NonNull StatementContext<?> statementContext, 057 @NonNull ResultSet resultSet, 058 @NonNull Object resultSetValue, 059 @NonNull TargetType targetType, 060 @NonNull Integer columnIndex, 061 @Nullable String columnLabel, 062 @NonNull InstanceProvider instanceProvider) throws SQLException; 063 064 /** 065 * Specifies which types this mapper should handle. 066 * <p> 067 * For example, if this mapper should apply when marshaling to {@code MyCustomType}, this method could return {@code targetType.matchesClass(MyCustomType.class)}. 068 * <p> 069 * For parameterized types like {@code List<UUID>}, this method could return {@code targetType.matchesParameterizedType(List.class, UUID.class)}. 070 * 071 * @param targetType the target type to evaluate - should this mapper handle it or not? 072 * @return {@code true} if this mapper should handle the type, {@code false} otherwise. 073 */ 074 @NonNull 075 Boolean appliesTo(@NonNull TargetType targetType); 076 077 /** 078 * Result of a custom column mapping attempt. 079 * <p> 080 * Use {@link #of(Object)} to indicate a successfully mapped value or {@link #fallback()} to indicate "didn't map; fall back to the registered {@link ResultSetMapper} behavior".</p> 081 */ 082 @ThreadSafe 083 sealed abstract class MappingResult permits MappingResult.CustomMapping, MappingResult.Fallback { 084 private MappingResult() {} 085 086 /** 087 * Indicates a successfully-mapped custom value. 088 * 089 * @param value the custom value, may be {@code null} 090 * @return a result which indicates a successfully-mapped custom value 091 */ 092 @NonNull 093 public static MappingResult of(@Nullable Object value) { 094 return new CustomMapping(value); 095 } 096 097 /** 098 * Indicates that this mapper did not map a custom value and prefers to fall back to the behavior of the registered {@link ResultSetMapper}. 099 * 100 * @return a result which indicates that this mapper did not map a custom value 101 */ 102 @NonNull 103 public static MappingResult fallback() { 104 return Fallback.INSTANCE; 105 } 106 107 @ThreadSafe 108 static final class CustomMapping extends MappingResult { 109 @Nullable 110 private final Object value; 111 112 private CustomMapping(@Nullable Object value) { 113 this.value = value; 114 } 115 116 @NonNull 117 public Optional<Object> getValue() { 118 return Optional.ofNullable(value); 119 } 120 } 121 122 @ThreadSafe 123 static final class Fallback extends MappingResult { 124 static final Fallback INSTANCE = new Fallback(); 125 126 private Fallback() {} 127 } 128 } 129}