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.commons.beanquery; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.lang.reflect.*; 022import java.time.temporal.*; 023import java.util.*; 024 025import org.apache.juneau.commons.bean.*; 026 027/** 028 * One bean property as BeanQuery sees it: its column name, value type and inferred {@link SearchType}, and how to read 029 * and write it. 030 * 031 * <p> 032 * Instances come from {@link QueryModel}. The name is the Juneau bean property name, after {@link BeanProp @BeanProp} 033 * and the configured {@link PropertyNamer}, so it is the name serializers emit. Reads use the getter (or record 034 * accessor), else the field. Writes use the setter, else a non-final field. 035 * 036 * <h5 class='section'>Example:</h5> 037 * <p class='bjava'> 038 * QueryProperty <jv>age</jv> = QueryModel.<jsm>of</jsm>(Person.<jk>class</jk>).getReadableProperty(<js>"age"</js>); 039 * SearchType <jv>type</jv> = <jv>age</jv>.getSearchType(); <jc>// NUMERIC</jc> 040 * Object <jv>value</jv> = <jv>age</jv>.read(<jv>person</jv>); 041 * </p> 042 * 043 * @since 10.0.0 044 */ 045public final class QueryProperty { 046 047 private final String name; 048 private final Method getter; 049 private final Method setter; 050 private final Field field; 051 private final Class<?> type; 052 private final Class<?> writeType; 053 private final SearchType searchType; 054 private final boolean writeOnly; // @BeanProp(wo) / writeOnlyProperties. 055 056 QueryProperty(BeanPropertyMeta p) { 057 name = p.getName(); 058 writeOnly = p.isWriteOnly(); 059 getter = p.getGetter() == null ? null : p.getGetter().inner(); 060 setter = p.getSetter() == null ? null : p.getSetter().inner(); 061 field = p.getField() == null ? null : p.getField().inner(); 062 if (nn(getter)) 063 type = getter.getReturnType(); 064 else if (nn(field)) 065 type = field.getType(); 066 else if (nn(setter)) 067 type = setter.getParameterTypes()[0]; 068 else 069 throw isex("Property '%s' has no getter, setter or field.", name); 070 writeType = nn(setter) ? setter.getParameterTypes()[0] : type; 071 searchType = searchTypeOf(type); 072 } 073 074 /** 075 * Infers a column's {@link SearchType} from a property's declared type. 076 * 077 * <p> 078 * Booleans are {@link SearchType#BOOLEAN}; characters are {@link SearchType#TEXT}; primitives and {@link Number}s 079 * are {@link SearchType#NUMERIC}; enums are {@link SearchType#ENUM}; dates, calendars and temporals are 080 * {@link SearchType#TIMESTAMP}; everything else is {@link SearchType#TEXT}. 081 * 082 * <h5 class='section'>Example:</h5> 083 * <p class='bjava'> 084 * QueryProperty.<jsm>searchTypeOf</jsm>(<jk>int</jk>.<jk>class</jk>); <jc>// NUMERIC</jc> 085 * QueryProperty.<jsm>searchTypeOf</jsm>(Instant.<jk>class</jk>); <jc>// TIMESTAMP</jc> 086 * QueryProperty.<jsm>searchTypeOf</jsm>(String.<jk>class</jk>); <jc>// TEXT</jc> 087 * </p> 088 * 089 * @param t The property type. Must not be <jk>null</jk>. 090 * @return The inferred type (never <jk>null</jk>). 091 */ 092 public static SearchType searchTypeOf(Class<?> t) { 093 if (t == boolean.class || t == Boolean.class) 094 return SearchType.BOOLEAN; 095 if (t == char.class || t == Character.class) 096 return SearchType.TEXT; 097 if (t.isPrimitive() || Number.class.isAssignableFrom(t)) 098 return SearchType.NUMERIC; 099 if (t.isEnum()) 100 return SearchType.ENUM; 101 if (Date.class.isAssignableFrom(t) || Calendar.class.isAssignableFrom(t) || TemporalAccessor.class.isAssignableFrom(t)) 102 return SearchType.TIMESTAMP; 103 return SearchType.TEXT; 104 } 105 106 /** 107 * Returns the property (column) name. 108 * 109 * @return The name. Never <jk>null</jk>. 110 */ 111 public String getName() { return name; } 112 113 /** 114 * Returns the declared read type: the getter's return type, else the field type, else the setter's parameter type. 115 * 116 * @return The type. Never <jk>null</jk>. 117 */ 118 public Class<?> getType() { return type; } 119 120 /** 121 * Returns the type {@link #write(Object, Object)} accepts: the setter's parameter type, else {@link #getType()}. 122 * 123 * @return The type. Never <jk>null</jk>. 124 */ 125 public Class<?> getWriteType() { return writeType; } 126 127 /** 128 * Returns the search type inferred from {@link #getType()}. 129 * 130 * @return The search type. Never <jk>null</jk>. 131 */ 132 public SearchType getSearchType() { return searchType; } 133 134 /** 135 * Returns <jk>true</jk> if the property has a getter or a field and isn't write-only. 136 * 137 * @return <jk>true</jk> if {@link #read(Object)} is supported. 138 */ 139 public boolean canRead() { return ! writeOnly && (nn(getter) || nn(field)); } 140 141 /** 142 * Returns <jk>true</jk> if the property has a setter or a non-final field. 143 * 144 * <p> 145 * {@link BeanProp#ro() @BeanProp(ro)} is deliberately ignored: in Juneau it means "parsers must not set this 146 * property", and loading a database row is not parsing. 147 * 148 * @return <jk>true</jk> if {@link #write(Object, Object)} is supported. 149 */ 150 public boolean canWrite() { return (nn(setter) || (nn(field) && ! Modifier.isFinal(field.getModifiers()))); } 151 152 /** 153 * Reads the property from a bean. 154 * 155 * @param bean The bean. Must not be <jk>null</jk>. 156 * @return The value. Can be <jk>null</jk>. 157 * @throws BeanQueryExecutionException If the getter throws or the value can't be read. The cause is the getter's 158 * own exception. 159 * @throws IllegalArgumentException If <c>bean</c> is <jk>null</jk>. 160 * @throws IllegalStateException If the property is not readable. 161 */ 162 public Object read(Object bean) { 163 reqnn("bean", bean); 164 if (! canRead()) 165 throw isex("Property '%s' is not readable.", name); 166 try { 167 return nn(getter) ? getter.invoke(bean) : field.get(bean); 168 } catch (InvocationTargetException e) { 169 throw new BeanQueryExecutionException(e.getCause(), "Failed to read column '%s'.", name); 170 } catch (IllegalAccessException | IllegalArgumentException e) { 171 throw new BeanQueryExecutionException(e, "Failed to read column '%s'.", name); 172 } 173 } 174 175 /** 176 * Writes the property on a bean. A <jk>null</jk> for a primitive property is ignored, so the bean keeps its 177 * default. 178 * 179 * @param bean The bean. Must not be <jk>null</jk>. 180 * @param value The value, assignable to {@link #getWriteType()}. Can be <jk>null</jk>. 181 * @throws BeanQueryExecutionException If the setter throws or the value can't be written. The cause is the 182 * setter's own exception. 183 * @throws IllegalArgumentException If <c>bean</c> is <jk>null</jk>. 184 * @throws IllegalStateException If the property is not writable. 185 */ 186 @SuppressWarnings({ 187 "java:S3011" // Field.set on a public field; no accessibility override, reflection is the point of this class. 188 }) 189 public void write(Object bean, Object value) { 190 reqnn("bean", bean); 191 if (! canWrite()) 192 throw isex("Property '%s' is not writable.", name); 193 if (value == null && writeType.isPrimitive()) 194 return; 195 try { 196 if (nn(setter)) 197 setter.invoke(bean, value); 198 else 199 field.set(bean, value); 200 } catch (InvocationTargetException e) { 201 throw new BeanQueryExecutionException(e.getCause(), "Failed to write column '%s'.", name); 202 } catch (IllegalAccessException | IllegalArgumentException e) { 203 throw new BeanQueryExecutionException(e, "Failed to write column '%s'.", name); 204 } 205 } 206 207 @Override /* Object */ 208 public String toString() { 209 return name + ":" + searchType; 210 } 211}