001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.beanquery.sql; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.commons.utils.StringUtils.*; 021 022import java.sql.*; 023import java.time.*; 024import java.util.*; 025 026import org.apache.juneau.commons.bean.*; 027import org.apache.juneau.commons.beanquery.*; 028 029/** 030 * Turns the current {@link ResultSet} row into a bean, for {@link SqlBeanQuerySession#find(BeanQuery)} and 031 * {@link SqlBeanQuerySession#stream(BeanQuery)}. 032 * 033 * <p> 034 * The result set is positioned on the row; do not call {@link ResultSet#next()}. {@code columns} lists the selected 035 * (logical) column names in {@code SELECT} order (the query's view, or every column), so column {@code i} of the 036 * result set is {@code columns.get(i - 1)} and a mapper can skip columns the query did not select. 037 * 038 * <p> 039 * Two built-in mappers need no dependency beyond this module: 040 * <ul> 041 * <li>{@link #map()} — each row as a {@link LinkedHashMap} keyed by column name in view order. 042 * <li>{@link #bean(Class)} — each row as a {@code record} (canonical constructor) or as a class with a public 043 * no-arg constructor and public setters ({@code setX}/{@code withX}, void or fluent) or public non-final fields. 044 * Columns are matched to Juneau bean property names by exact name; columns with no property are ignored, and 045 * properties with no column keep their defaults. 046 * <li>{@link #bean(BeanMeta)} — the same, but using existing bean metadata, so custom property namers apply and 047 * column names match the property names a serializer built from that metadata emits. 048 * </ul> 049 * 050 * <p> 051 * When no mapper is set on the {@link SqlBeanQueryContext.Builder}, one is chosen from the row type: a {@code Map} 052 * type gets {@link #map()}, and any other type that {@link #bean(Class)} can plan gets {@link #bean(Class)}. 053 * {@code Object}, {@code Map} types a {@link LinkedHashMap} is not assignable to (such as {@code TreeMap}) and types 054 * that cannot be planned get none, and {@code find}/{@code stream} then throw {@link IllegalStateException}. 055 * 056 * <h5 class='section'>Example:</h5> 057 * <p class='bjava'> 058 * <jc>// Custom mapper.</jc> 059 * SqlRowMapper<Person> <jv>mapper</jv> = (<jv>rs</jv>, <jv>columns</jv>) -> <jk>new</jk> Person( 060 * <jv>columns</jv>.contains(<js>"name"</js>) ? <jv>rs</jv>.getString(<js>"full_name"</js>) : <jk>null</jk>, 061 * <jv>columns</jv>.contains(<js>"age"</js>) ? <jv>rs</jv>.getInt(<js>"age"</js>) : 0); 062 * 063 * <jc>// Rows as maps, or as a record / setter bean.</jc> 064 * SqlRowMapper<Map<String,Object>> <jv>maps</jv> = SqlRowMapper.<jsm>map</jsm>(); 065 * SqlRowMapper<Person> <jv>beans</jv> = SqlRowMapper.<jsm>bean</jsm>(Person.<jk>class</jk>); 066 * 067 * <jc>// Default selection: no rowMapper(...) call, so Person (a record) gets bean(Person.class).</jc> 068 * SqlBeanQueryContext<Person> <jv>context</jv> = SqlBeanQueryContext.<jsm>create</jsm>(Person.<jk>class</jk>) 069 * <jc>// ...dialect, table, columns, connection source...</jc> 070 * .build(); 071 * </p> 072 * 073 * @param <T> The row (bean) type. 074 * @since 10.0.0 075 */ 076@FunctionalInterface 077public interface SqlRowMapper<T> { 078 079 /** 080 * Maps the current result-set row to a bean. 081 * 082 * @param rs The result set, positioned on the row to map. Must not be advanced. 083 * @param columns The selected column names in {@code SELECT} order. 084 * @return The mapped bean. 085 * @throws SQLException If a column read fails. 086 */ 087 T map(ResultSet rs, List<String> columns) throws SQLException; 088 089 /** 090 * A built-in row mapper that maps each row to a new {@link LinkedHashMap}, keyed by the logical column name in 091 * {@code SELECT} order, with each value read via {@link ResultSet#getObject(int)} (the driver's natural type; 092 * <jk>null</jk> for SQL {@code NULL}). 093 * 094 * <h5 class='section'>Example:</h5> 095 * <p class='bjava'> 096 * SqlRowMapper<Map<String,Object>> <jv>mapper</jv> = SqlRowMapper.<jsm>map</jsm>(); 097 * </p> 098 * 099 * @return A shared, stateless row mapper. Always the same instance. 100 * @since 10.0.0 101 */ 102 static SqlRowMapper<Map<String,Object>> map() { 103 return SqlRowMappers.MAP; 104 } 105 106 /** 107 * A built-in row mapper that maps each row to a new instance of {@code type}: a {@code record} (via its 108 * canonical constructor) or a concrete class with a public no-arg constructor and public setters 109 * ({@code setX}/{@code withX} returning {@code void} or the bean type, so fluent setters work) or public non-final 110 * fields. 111 * 112 * <p> 113 * Planning happens here, once; mapping never re-inspects {@code type}. Columns match Juneau bean property names 114 * exactly (setters, public fields and record components, with {@code @BeanIgnore}, {@code @BeanProp} and the 115 * default property namer applied; see {@link QueryModel}). A column with no 116 * property is ignored; a record component with no column, or a SQL {@code NULL} for a primitive, gets the type's 117 * default. Values are read with {@code ResultSet.getObject(i, type)}, except {@link Instant} (via 118 * {@link OffsetDateTime}), {@link java.util.Date} (via {@code getTimestamp}) and enums (by constant name). 119 * A failure reading or setting a column throws a {@link BeanQueryExecutionException} naming the column. The 120 * returned mapper is thread-safe. 121 * 122 * <h5 class='section'>Example:</h5> 123 * <p class='bjava'> 124 * <jk>record</jk> Task(String name, <jk>int</jk> age) {} 125 * SqlRowMapper<Task> <jv>mapper</jv> = SqlRowMapper.<jsm>bean</jsm>(Task.<jk>class</jk>); 126 * </p> 127 * 128 * @param <T> The bean type. 129 * @param type The bean type. Must not be <jk>null</jk>. 130 * @return A row mapper for {@code type}. 131 * @throws IllegalArgumentException If {@code type} cannot be planned: an interface, abstract class, enum, array or 132 * primitive type, a class with no public no-arg constructor, or a setter bean with an ambiguous (overloaded) setter. 133 * @since 10.0.0 134 */ 135 static <T> SqlRowMapper<T> bean(Class<T> type) { 136 reqnn("type", type); 137 var plan = SqlRowMappers.tryBean(type); 138 if (plan.reason() != null) 139 throw new IllegalArgumentException(format("Cannot map rows to '%s': %s.", type.getSimpleName(), plan.reason())); 140 return plan.mapper(); 141 } 142 143 /** 144 * Same as {@link #bean(Class)}, but follows the bean rules of existing bean metadata, for example the metadata of 145 * the serializer that renders the rows, so column names match the property names it emits. 146 * 147 * <h5 class='section'>Example:</h5> 148 * <p class='bjava'> 149 * BeanConfigContext <jv>config</jv> = BeanConfigContext.<jsm>create</jsm>().propertyNamer(PropertyNamerDLC.<jsf>INSTANCE</jsf>).build(); 150 * SqlRowMapper<Task> <jv>mapper</jv> = SqlRowMapper.<jsm>bean</jsm>(BeanMeta.<jsm>of</jsm>(Task.<jk>class</jk>, <jv>config</jv>)); 151 * </p> 152 * 153 * @param <T> The bean type. 154 * @param beanMeta The bean metadata. Must not be <jk>null</jk>. 155 * @return A row mapper for the bean type. 156 * @throws IllegalArgumentException For the same reasons as {@link #bean(Class)}. 157 * @since 10.0.0 158 */ 159 static <T> SqlRowMapper<T> bean(BeanMeta<T> beanMeta) { 160 reqnn("beanMeta", beanMeta); 161 var plan = SqlRowMappers.tryBean(beanMeta); 162 if (plan.reason() != null) 163 throw new IllegalArgumentException(format("Cannot map rows to '%s': %s.", beanMeta.getClassInfo().inner().getSimpleName(), plan.reason())); 164 return plan.mapper(); 165 } 166}