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.reflect.ReflectionUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.lang.reflect.*; 023import java.util.*; 024 025import org.apache.juneau.commons.bean.*; 026 027/** 028 * The query view of a bean class: its readable properties (query columns), its writable properties, and how to create 029 * an instance. 030 * 031 * <p> 032 * Built from Juneau's {@link BeanMeta}, so it follows the same bean rules as serializers and parsers: 033 * <ul> 034 * <li>getters, setters and fields; 035 * <li>records; 036 * <li>{@link BeanIgnore @BeanIgnore} and {@link BeanProp @BeanProp}; 037 * <li>the configured {@link PropertyNamer}. 038 * </ul> 039 * Dynamic ({@code "*"}) properties are excluded. A class that isn't a bean (for example, one with no properties) 040 * yields an empty model rather than an error. 041 * 042 * <h5 class='section'>Example:</h5> 043 * <p class='bjava'> 044 * QueryModel<Person> <jv>model</jv> = QueryModel.<jsm>of</jsm>(Person.<jk>class</jk>); 045 * <jk>for</jk> (QueryProperty <jv>p</jv> : <jv>model</jv>.getReadableProperties().values()) 046 * System.<jsf>out</jsf>.println(<jv>p</jv>.getName() + <js>" "</js> + <jv>p</jv>.getSearchType()); 047 * 048 * <jc>// Same rules as a serializer configured with a dashed-lower-case namer.</jc> 049 * BeanConfigContext <jv>config</jv> = BeanConfigContext.<jsm>create</jsm>().propertyNamer(PropertyNamerDLC.<jsf>INSTANCE</jsf>).build(); 050 * QueryModel<Person> <jv>dashed</jv> = QueryModel.<jsm>of</jsm>(Person.<jk>class</jk>, <jv>config</jv>); 051 * </p> 052 * 053 * @param <T> The bean type. 054 * @since 10.0.0 055 */ 056public final class QueryModel<T> { 057 058 /** 059 * Builds a model with {@link BeanConfigContext#DEFAULT}. 060 * 061 * @param <T> The bean type. 062 * @param beanClass The bean class. Must not be <jk>null</jk>. 063 * @return A new model. 064 */ 065 public static <T> QueryModel<T> of(Class<T> beanClass) { 066 return of(beanClass, BeanConfigContext.DEFAULT); 067 } 068 069 /** 070 * Builds a model with the given bean configuration. 071 * 072 * @param <T> The bean type. 073 * @param beanClass The bean class. Must not be <jk>null</jk>. 074 * @param config The bean configuration. Must not be <jk>null</jk>. 075 * @return A new model. 076 */ 077 public static <T> QueryModel<T> of(Class<T> beanClass, BeanConfigContext config) { 078 return of(BeanMeta.of(reqnn("beanClass", beanClass), reqnn("config", config))); 079 } 080 081 /** 082 * Builds a model from existing bean metadata. 083 * 084 * @param <T> The bean type. 085 * @param beanMeta The bean metadata. Must not be <jk>null</jk>. 086 * @return A new model. 087 */ 088 public static <T> QueryModel<T> of(BeanMeta<T> beanMeta) { 089 return new QueryModel<>(reqnn("beanMeta", beanMeta)); 090 } 091 092 private final Class<T> beanClass; 093 private final Map<String,QueryProperty> properties; 094 private final Map<String,QueryProperty> writableProperties; 095 private final Constructor<?> constructor; 096 private final List<String> constructorArgs; 097 private final List<Class<?>> constructorArgTypes; 098 099 private QueryModel(BeanMeta<T> bm) { 100 beanClass = bm.getClassInfo().inner(); 101 var r = new LinkedHashMap<String,QueryProperty>(); 102 var w = new LinkedHashMap<String,QueryProperty>(); 103 bm.getProperties().forEach((name, p) -> { 104 if (p.isDyna()) 105 return; 106 var qp = new QueryProperty(p); 107 if (qp.canRead()) 108 r.put(name, qp); 109 if (qp.canWrite()) 110 w.put(name, qp); 111 }); 112 properties = Collections.unmodifiableMap(r); 113 writableProperties = Collections.unmodifiableMap(w); 114 // Non-static inner classes need an outer instance, which would misalign the constructor args with their types. 115 var ci = bm.getClassInfo().isNonStaticMemberClass() ? null : bm.getConstructor(); 116 constructor = ci == null ? null : ci.inner(); 117 constructorArgs = ci == null ? List.of() : List.copyOf(bm.getConstructorArgs()); 118 constructorArgTypes = constructor == null ? List.of() : List.of(constructor.getParameterTypes()); 119 } 120 121 /** 122 * Returns the bean class. 123 * 124 * @return The bean class. Never <jk>null</jk>. 125 */ 126 public Class<T> getBeanClass() { return beanClass; } 127 128 /** 129 * Returns the readable properties (the query columns), keyed by name, in bean property order. Write-only 130 * properties are excluded. 131 * 132 * @return An unmodifiable map. Never <jk>null</jk>. 133 */ 134 public Map<String,QueryProperty> getReadableProperties() { return properties; } 135 136 /** 137 * Returns a readable property. Write-only properties are excluded. 138 * 139 * @param name The property name. 140 * @return The property, or <jk>null</jk> if there is no readable property with that name. 141 */ 142 public QueryProperty getReadableProperty(String name) { return properties.get(name); } 143 144 /** 145 * Returns the writable properties (setter or non-final field), keyed by name, in bean property order. Read-only 146 * properties are excluded. 147 * 148 * <p> 149 * Record components are not listed here; they are set through the constructor (see {@link #getConstructorArgs()}). 150 * 151 * @return An unmodifiable map. Never <jk>null</jk>. 152 */ 153 public Map<String,QueryProperty> getWritableProperties() { return writableProperties; } 154 155 /** 156 * Returns the names of the properties passed to the constructor, in parameter order. 157 * 158 * <p> 159 * Non-empty for records (canonical constructor) and {@link BeanCtor @BeanCtor} classes; empty for no-arg beans. 160 * 161 * @return An unmodifiable list. Never <jk>null</jk>. 162 */ 163 public List<String> getConstructorArgs() { return constructorArgs; } 164 165 /** 166 * Returns the constructor parameter types, parallel to {@link #getConstructorArgs()}. 167 * 168 * @return An unmodifiable list. Never <jk>null</jk>. 169 */ 170 public List<Class<?>> getConstructorArgTypes() { return constructorArgTypes; } 171 172 /** 173 * Creates an instance. 174 * 175 * <p> 176 * Non-static inner classes are not supported (they need an outer instance); the model reports no usable 177 * constructor for them. 178 * 179 * @param args The constructor arguments, parallel to {@link #getConstructorArgs()} (none for a no-arg bean). A 180 * <jk>null</jk> for a primitive parameter becomes that primitive's default. To pass a single <jk>null</jk> 181 * argument, pass <c>(Object)<jk>null</jk></c>. 182 * @return A new instance. Never <jk>null</jk>. 183 * @throws IllegalArgumentException If the args array is <jk>null</jk>, or the argument count doesn't match {@link #getConstructorArgs()}. 184 * @throws IllegalStateException If the class has no usable constructor. 185 * @throws BeanQueryExecutionException If the constructor throws, or the arguments can't be passed to it (for 186 * example, a wrong type). The cause is the constructor's own exception, or the reflection error. 187 */ 188 public T newInstance(Object...args) { 189 reqnn("args", args); 190 if (constructor == null) 191 throw isex("%s has no usable constructor.", beanClass.getSimpleName()); 192 if (args.length != constructorArgs.size()) 193 throw iaex("Expected %s constructor arguments for %s but got %s.", constructorArgs.size(), beanClass.getSimpleName(), args.length); 194 var a = args.clone(); 195 for (var i = 0; i < a.length; i++) 196 if (a[i] == null && constructorArgTypes.get(i).isPrimitive()) 197 a[i] = info(constructorArgTypes.get(i)).getPrimitiveDefault(); 198 try { 199 return beanClass.cast(constructor.newInstance(a)); 200 } catch (InvocationTargetException e) { 201 throw new BeanQueryExecutionException(e.getCause(), "Failed to construct a %s.", beanClass.getSimpleName()); 202 } catch (ReflectiveOperationException | RuntimeException e) { 203 throw new BeanQueryExecutionException(e, "Failed to construct a %s.", beanClass.getSimpleName()); 204 } 205 } 206}