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}