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