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.bct; 018 019import static org.apache.juneau.commons.utils.ObjectUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021import static org.apache.juneau.commons.utils.ThrowableUtils.*; 022 023import java.lang.reflect.*; 024import java.util.*; 025 026import org.apache.juneau.commons.utils.*; 027 028/** 029 * Collection of standard property extractor implementations for the Bean-Centric Testing framework. 030 * 031 * <p>This class provides the built-in property extraction strategies that handle the most common 032 * object types and property access patterns. These extractors are automatically registered when 033 * using {@link BasicBeanConverter.Builder#defaultSettings()}.</p> 034 * 035 * <h5 class='section'>Extractor Hierarchy:</h5> 036 * <p>The extractors form an inheritance hierarchy for code reuse:</p> 037 * <ul> 038 * <li><b>{@link ObjectPropertyExtractor}</b> - Base class with JavaBean property access</li> 039 * <li><b>{@link ListPropertyExtractor}</b> - Extends ObjectPropertyExtractor with array/collection support</li> 040 * <li><b>{@link MapPropertyExtractor}</b> - Extends ObjectPropertyExtractor with Map key access</li> 041 * </ul> 042 * 043 * <h5 class='section'>Execution Order:</h5> 044 * <p>In {@link BasicBeanConverter}, the extractors are tried in this order:</p> 045 * <ol> 046 * <li><b>Custom extractors</b> - User-registered extractors via {@link BasicBeanConverter.Builder#addPropertyExtractor(PropertyExtractor)}</li> 047 * <li><b>{@link ObjectPropertyExtractor}</b> - JavaBean properties, fields, and methods</li> 048 * <li><b>{@link ListPropertyExtractor}</b> - Array/collection indices and size properties</li> 049 * <li><b>{@link MapPropertyExtractor}</b> - Map key access and size property</li> 050 * </ol> 051 * 052 * <h5 class='section'>Property Access Strategy:</h5> 053 * <p>Each extractor implements a comprehensive fallback strategy for maximum compatibility:</p> 054 * 055 * @see PropertyExtractor 056 * @see BasicBeanConverter.Builder#defaultSettings() 057 * @see BasicBeanConverter.Builder#addPropertyExtractor(PropertyExtractor) 058 */ 059public class PropertyExtractors { 060 061 /** 062 * Property extractor for array and collection objects with numeric indexing and size access. 063 * 064 * <p>This extractor extends {@link ObjectPropertyExtractor} to add special handling for 065 * collection-like objects. It provides array-style access using numeric indices and 066 * universal size/length properties for any listifiable object.</p> 067 * 068 * <h5 class='section'>Additional Properties:</h5> 069 * <ul> 070 * <li><b>Numeric indices:</b> <js>"0"</js>, <js>"1"</js>, <js>"2"</js>, etc. for element access</li> 071 * <li><b>Negative indices:</b> <js>"-1"</js>, <js>"-2"</js> for reverse indexing (from end)</li> 072 * <li><b>Size properties:</b> <js>"length"</js> and <js>"size"</js> return collection size</li> 073 * </ul> 074 * 075 * <h5 class='section'>Supported Types:</h5> 076 * <p>Works with any object that can be listified by the converter:</p> 077 * <ul> 078 * <li><b>Arrays:</b> All array types (primitive and object)</li> 079 * <li><b>Collections:</b> List, Set, Queue, and all Collection subtypes</li> 080 * <li><b>Iterables:</b> Any object implementing Iterable</li> 081 * <li><b>Streams:</b> Stream objects and other lazy sequences</li> 082 * <li><b>Maps:</b> Converted to list of entries for iteration</li> 083 * </ul> 084 * 085 * <h5 class='section'>Examples:</h5> 086 * <p class='bjava'> 087 * <jc>// Array/List access</jc> 088 * <jv>list</jv>.get(0) <jc>// "0" property</jc> 089 * <jv>array</jv>[2] <jc>// "2" property</jc> 090 * <jv>list</jv>.get(-1) <jc>// "-1" property (last element)</jc> 091 * 092 * <jc>// Size access</jc> 093 * <jv>array</jv>.length <jc>// "length" property</jc> 094 * <jv>collection</jv>.size() <jc>// "size" property</jc> 095 * <jv>stream</jv>.count() <jc>// "length" or "size" property</jc> 096 * </p> 097 * 098 * <p><b>Fallback:</b> If the property is not a numeric index or size property, 099 * delegates to {@link ObjectPropertyExtractor} for standard property access.</p> 100 */ 101 public static class ListPropertyExtractor extends ObjectPropertyExtractor { 102 103 @Override 104 public boolean canExtract(BeanConverter converter, Object o, String name) { 105 return converter.canListify(o); 106 } 107 108 @Override 109 public Object extract(BeanConverter converter, Object o, String name) { 110 var l = converter.listify(o); 111 if (name.matches("-?\\d+")) { 112 var index = StringUtils.parseInt(name); 113 if (index < 0) { 114 index = l.size() + index; // Convert negative index to positive 115 } 116 return l.get(index); 117 } 118 if (eq(name, "length")) 119 return l.size(); 120 if (eq(name, "size")) 121 return l.size(); 122 return super.extract(converter, o, name); 123 } 124 } 125 126 /** 127 * Property extractor for Map objects with direct key access and size property. 128 * 129 * <p>This extractor extends {@link ObjectPropertyExtractor} to add special handling for 130 * Map objects. It provides direct key-based property access and a universal size 131 * property for Map objects.</p> 132 * 133 * <h5 class='section'>Map-Specific Properties:</h5> 134 * <ul> 135 * <li><b>Direct key access:</b> Any property name that exists as a Map key</li> 136 * <li><b>Size property:</b> {@code "size"} returns {@code map.size()}</li> 137 * </ul> 138 * 139 * <h5 class='section'>Supported Types:</h5> 140 * <p>Works with any object implementing the {@code Map} interface:</p> 141 * <ul> 142 * <li><b>HashMap, LinkedHashMap:</b> Standard Map implementations</li> 143 * <li><b>TreeMap, ConcurrentHashMap:</b> Specialized Map implementations</li> 144 * <li><b>Properties:</b> Java Properties objects</li> 145 * <li><b>Custom Maps:</b> Any Map implementation</li> 146 * </ul> 147 * 148 * <h5 class='section'>Examples:</h5> 149 * <p class='bjava'> 150 * <jc>// Direct key access</jc> 151 * <jv>map</jv>.get(<js>"name"</js>) <jc>// "name" property</jc> 152 * <jv>map</jv>.get(<js>"timeout"</js>) <jc>// "timeout" property</jc> 153 * <jv>props</jv>.getProperty(<js>"key"</js>) <jc>// "key" property</jc> 154 * 155 * <jc>// Size access</jc> 156 * <jv>map</jv>.size() <jc>// "size" property</jc> 157 * </p> 158 * 159 * <h5 class='section'>Key Priority:</h5> 160 * <p>Map key access takes priority over JavaBean properties. If a Map contains 161 * a key with the same name as a property/method, the Map value is returned first.</p> 162 * 163 * <p><b>Fallback:</b> If the property is not found as a Map key and is not "size", 164 * delegates to {@link ObjectPropertyExtractor} for standard property access.</p> 165 */ 166 public static class MapPropertyExtractor extends ObjectPropertyExtractor { 167 168 @Override 169 public boolean canExtract(BeanConverter converter, Object o, String name) { 170 return o instanceof Map; 171 } 172 173 @Override 174 public Object extract(BeanConverter converter, Object o, String name) { 175 var m = (Map<?,?>)o; 176 if (eq(name, converter.getSetting(BasicBeanConverter.SETTING_nullValue, "<null>"))) 177 name = null; 178 if (m.containsKey(name)) 179 return m.get(name); 180 if (eq(name, "size")) 181 return m.size(); 182 return super.extract(converter, o, name); 183 } 184 } 185 186 /** 187 * Standard JavaBean property extractor using reflection. 188 * 189 * <p>This extractor serves as the universal fallback for property access, implementing 190 * comprehensive JavaBean property access patterns. It tries multiple approaches to 191 * access object properties, providing maximum compatibility with different coding styles.</p> 192 * 193 * <h5 class='section'>Property Access Order:</h5> 194 * <ol> 195 * <li><b>{@code is{Property}()}</b> - Boolean property getters (e.g., {@code isActive()})</li> 196 * <li><b>{@code get{Property}()}</b> - Standard getter methods (e.g., {@code getName()})</li> 197 * <li><b>{@code get(String)}</b> - Map-style property access with property name as parameter</li> 198 * <li><b>Fields</b> - Public fields with matching names (searches inheritance hierarchy)</li> 199 * <li><b>{@code {property}()}</b> - No-argument methods with exact property name</li> 200 * </ol> 201 * 202 * <h5 class='section'>Examples:</h5> 203 * <p class='bjava'> 204 * <jc>// Property "name" can be accessed via:</jc> 205 * <jv>obj</jv>.getName() <jc>// Standard getter</jc> 206 * <jv>obj</jv>.name <jc>// Public field</jc> 207 * <jv>obj</jv>.name() <jc>// Method with property name</jc> 208 * <jv>obj</jv>.get(<js>"name"</js>) <jc>// Map-style getter</jc> 209 * 210 * <jc>// Property "active" (boolean) can be accessed via:</jc> 211 * <jv>obj</jv>.isActive() <jc>// Boolean getter</jc> 212 * <jv>obj</jv>.getActive() <jc>// Standard getter alternative</jc> 213 * <jv>obj</jv>.active <jc>// Public field</jc> 214 * </p> 215 * 216 * <p><b>Compatibility:</b> This extractor can handle any object type, making it the 217 * universal fallback. It always returns {@code true} from {@link #canExtract(BeanConverter, Object, String)}.</p> 218 */ 219 public static class ObjectPropertyExtractor implements PropertyExtractor { 220 221 @Override 222 public boolean canExtract(BeanConverter converter, Object o, String name) { 223 return true; 224 } 225 226 @SuppressWarnings({ 227 "java:S3011", // Reflection access required for framework functionality 228 "java:S3776", // Cognitive complexity acceptable for this specific logic 229 "null" // Intentional brain method. 230 }) 231 @Override 232 public Object extract(BeanConverter converter, Object o, String name) { 233 return safe(() -> { 234 if (o == null) 235 return null; 236 var f = nullObject(Field.class); 237 var c = o.getClass(); 238 var n = Character.toUpperCase(name.charAt(0)) + name.substring(1); 239 var m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals("is" + n) && x.getParameterCount() == 0).findFirst().orElse(null); 240 if (nn(m)) { 241 m.setAccessible(true); 242 return m.invoke(o); 243 } 244 if (o instanceof Map.Entry<?,?> me) { 245 // Reflection to classes inside java.util are restricted in Java 9+. 246 if (eq(name, "key")) 247 return me.getKey(); 248 if (eq(name, "value")) 249 return me.getValue(); 250 } 251 m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals("get" + n) && x.getParameterCount() == 0).findFirst().orElse(null); 252 if (nn(m)) { 253 m.setAccessible(true); 254 return m.invoke(o); 255 } 256 m = Arrays.stream(c.getMethods()).filter(x -> eq(x.getName(), "get") && x.getParameterCount() == 1 && x.getParameterTypes()[0] == String.class).findFirst().orElse(null); 257 if (nn(m)) { 258 m.setAccessible(true); 259 return m.invoke(o, name); 260 } 261 var c2 = c; 262 while (f == null && nn(c2)) { 263 f = Arrays.stream(c2.getDeclaredFields()).filter(x -> x.getName().equals(name)).findFirst().orElse(null); 264 c2 = c2.getSuperclass(); 265 } 266 if (nn(f)) { 267 f.setAccessible(true); 268 return f.get(o); 269 } 270 m = Arrays.stream(c.getMethods()).filter(x -> x.getName().equals(name) && x.getParameterCount() == 0).findFirst().orElse(null); 271 if (nn(m)) { 272 m.setAccessible(true); 273 return m.invoke(o); 274 } 275 throw new PropertyNotFoundException(name, o.getClass()); 276 }); 277 } 278 } 279 280 private PropertyExtractors() {} 281}