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