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.util.Optional.*; 020import static java.util.stream.Collectors.*; 021import static org.apache.juneau.commons.reflect.ReflectionUtils.*; 022import static org.apache.juneau.commons.utils.AssertionUtils.*; 023import static org.apache.juneau.commons.utils.CollectionUtils.*; 024import static org.apache.juneau.commons.utils.ThrowableUtils.*; 025import static org.apache.juneau.commons.utils.Utils.*; 026 027import java.io.*; 028import java.lang.reflect.*; 029import java.util.*; 030import java.util.concurrent.*; 031import java.util.function.*; 032import java.util.stream.*; 033 034/** 035 * Default implementation of {@link BeanConverter} for Bean-Centric Test (BCT) object conversion. 036 * 037 * <p>This class provides a comprehensive, extensible framework for converting Java objects to strings 038 * and lists, with sophisticated property access capabilities. It's the core engine behind BCT testing 039 * assertions, handling complex object introspection and value extraction with high performance through 040 * intelligent caching and optimized lookup strategies.</p> 041 * 042 * <h5 class='section'>Key Features:</h5> 043 * <ul> 044 * <li><b>Extensible Type Handlers:</b> Pluggable stringifiers, listifiers, and swappers for custom types</li> 045 * <li><b>Performance Optimization:</b> ConcurrentHashMap caching for type-to-handler mappings</li> 046 * <li><b>Comprehensive Defaults:</b> Built-in support for all common Java types and structures</li> 047 * <li><b>Configurable Settings:</b> Customizable formatting, delimiters, and display options</li> 048 * <li><b>Thread Safety:</b> Fully thread-safe implementation suitable for concurrent testing</li> 049 * </ul> 050 * 051 * <h5 class='section'>Architecture Overview:</h5> 052 * <p>The converter uses four types of pluggable handlers:</p> 053 * <dl> 054 * <dt><b>Stringifiers:</b></dt> 055 * <dd>Convert objects to string representations with custom formatting rules</dd> 056 * 057 * <dt><b>Listifiers:</b></dt> 058 * <dd>Convert collection-like objects to List<Object> for uniform iteration</dd> 059 * 060 * <dt><b>Swappers:</b></dt> 061 * <dd>Pre-process objects before conversion (unwrap Optional, call Supplier, etc.)</dd> 062 * 063 * <dt><b>PropertyExtractors:</b></dt> 064 * <dd>Define custom property access strategies for nested field navigation (e.g., <js>"user.address.city"</js>)</dd> 065 * </dl> 066 * 067 * <p>PropertyExtractors use a chain-of-responsibility pattern, where each extractor in the chain 068 * is tried until one can handle the property access. The framework includes built-in extractors for:</p> 069 * <ul> 070 * <li><b>JavaBean properties:</b> Standard getter methods and public fields</li> 071 * <li><b>Collection/Array access:</b> Numeric indices and size/length properties</li> 072 * <li><b>Map access:</b> Key-based property retrieval and size property</li> 073 * </ul> 074 * 075 * <h5 class='section'>Default Type Support:</h5> 076 * <p>Out-of-the-box stringification support includes:</p> 077 * <ul> 078 * <li><b>Collections:</b> List, Set, Queue → <js>"[item1,item2,item3]"</js> format</li> 079 * <li><b>Maps:</b> Map, Properties → <js>"{key1=value1,key2=value2}"</js> format</li> 080 * <li><b>Map Entries:</b> Map.Entry → <js>"key=value"</js> format</li> 081 * <li><b>Arrays:</b> All array types → <js>"[element1,element2]"</js> format</li> 082 * <li><b>Dates:</b> Date, Calendar → ISO-8601 format</li> 083 * <li><b>Files/Streams:</b> File, InputStream, Reader → content as hex or text</li> 084 * <li><b>Reflection:</b> Class, Method, Constructor → human-readable signatures</li> 085 * <li><b>Enums:</b> Enum values → name() format</li> 086 * </ul> 087 * 088 * <p>Default listification support includes:</p> 089 * <ul> 090 * <li><b>Collection types:</b> List, Set, Queue, and all subtypes</li> 091 * <li><b>Iterable objects:</b> Any Iterable implementation</li> 092 * <li><b>Iterators:</b> Iterator and Enumeration (consumed to list)</li> 093 * <li><b>Streams:</b> Stream objects (terminated to list)</li> 094 * <li><b>Optional:</b> Empty list or single-element list</li> 095 * <li><b>Maps:</b> Converted to list of Map.Entry objects</li> 096 * </ul> 097 * 098 * <p>Default swapping support includes:</p> 099 * <ul> 100 * <li><b>Optional:</b> Unwrapped to contained value or <jk>null</jk></li> 101 * <li><b>Supplier:</b> Called to get supplied value</li> 102 * <li><b>Future:</b> Extracts completed result or returns <js>"<pending>"</js> for incomplete futures (via {@link Swappers#futureSwapper()})</li> 103 * </ul> 104 * 105 * <h5 class='section'>Configuration Settings:</h5> 106 * <p>The converter supports extensive customization via settings:</p> 107 * <dl> 108 * <dt><code>nullValue</code></dt> 109 * <dd>String representation for null values (default: <js>"<null>"</js>)</dd> 110 * 111 * <dt><code>selfValue</code></dt> 112 * <dd>Special property name that returns the object itself (default: <js>"<self>"</js>)</dd> 113 * 114 * <dt><code>emptyValue</code></dt> 115 * <dd>String representation for empty collections (default: <js>"<empty>"</js>)</dd> 116 * 117 * <dt><code>fieldSeparator</code></dt> 118 * <dd>Delimiter between collection elements and map entries (default: <js>","</js>)</dd> 119 * 120 * <dt><code>collectionPrefix/Suffix</code></dt> 121 * <dd>Brackets around collection content (default: <js>"["</js> and <js>"]"</js>)</dd> 122 * 123 * <dt><code>mapPrefix/Suffix</code></dt> 124 * <dd>Brackets around map content (default: <js>"{"</js> and <js>"}"</js>)</dd> 125 * 126 * <dt><code>mapEntrySeparator</code></dt> 127 * <dd>Separator between map keys and values (default: <js>"="</js>)</dd> 128 * 129 * <dt><code>calendarFormat</code></dt> 130 * <dd>DateTimeFormatter for calendar objects (default: <jsf>ISO_INSTANT</jsf>)</dd> 131 * 132 * <dt><code>classNameFormat</code></dt> 133 * <dd>Format for class names: <js>"simple"</js>, <js>"canonical"</js>, or <js>"full"</js> (default: <js>"simple"</js>)</dd> 134 * </dl> 135 * 136 * <h5 class='section'>Usage Examples:</h5> 137 * 138 * <p><b>Basic Usage with Defaults:</b></p> 139 * <p class='bjava'> 140 * <jc>// Use default converter</jc> 141 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsf>DEFAULT</jsf>; 142 * <jk>var</jk> <jv>result</jv> = <jv>converter</jv>.stringify(<jv>myObject</jv>); 143 * </p> 144 * 145 * <p><b>Custom Configuration:</b></p> 146 * <p class='bjava'> 147 * <jc>// Build custom converter</jc> 148 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 149 * .defaultSettings() 150 * .addSetting(<jsf>SETTING_nullValue</jsf>, <js>"<null>"</js>) 151 * .addSetting(<jsf>SETTING_fieldSeparator</jsf>, <js>" | "</js>) 152 * .addStringifier(MyClass.<jk>class</jk>, (<jp>obj</jp>, <jp>conv</jp>) -> <js>"MyClass["</js> + <jp>obj</jp>.getName() + <js>"]"</js>) 153 * .addListifier(MyIterable.<jk>class</jk>, (<jp>obj</jp>, <jp>conv</jp>) -> <jp>obj</jp>.toList()) 154 * .addSwapper(MyWrapper.<jk>class</jk>, (<jp>obj</jp>, <jp>conv</jp>) -> <jp>obj</jp>.getWrapped()) 155 * .build(); 156 * </p> 157 * 158 * <p><b>Complex Property Access:</b></p> 159 * <p class='bjava'> 160 * <jc>// Extract nested properties</jc> 161 * <jk>var</jk> <jv>name</jv> = <jv>converter</jv>.getEntry(<jv>user</jv>, <js>"name"</js>); 162 * <jk>var</jk> <jv>city</jv> = <jv>converter</jv>.getEntry(<jv>user</jv>, <js>"address.city"</js>); 163 * <jk>var</jk> <jv>firstOrder</jv> = <jv>converter</jv>.getEntry(<jv>user</jv>, <js>"orders.0.id"</js>); 164 * <jk>var</jk> <jv>orderCount</jv> = <jv>converter</jv>.getEntry(<jv>user</jv>, <js>"orders.length"</js>); 165 * </p> 166 * 167 * <p><b>Special Property Values:</b></p> 168 * <p class='bjava'> 169 * <jc>// Use special property names</jc> 170 * <jk>var</jk> <jv>userObj</jv> = <jv>converter</jv>.getEntry(<jv>user</jv>, <js>"<self>"</js>); <jc>// Returns the user object itself</jc> 171 * <jk>var</jk> <jv>nullValue</jv> = <jv>converter</jv>.getEntry(<jv>user</jv>, <js>"<null>"</js>); <jc>// Returns null</jc> 172 * 173 * <jc>// Custom self value</jc> 174 * <jk>var</jk> <jv>customConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 175 * .defaultSettings() 176 * .addSetting(<jsf>SETTING_selfValue</jsf>, <js>"this"</js>) 177 * .build(); 178 * <jk>var</jk> <jv>selfRef</jv> = <jv>customConverter</jv>.getEntry(<jv>user</jv>, <js>"this"</js>); <jc>// Returns user object</jc> 179 * </p> 180 * 181 * <h5 class='section'>Performance Characteristics:</h5> 182 * <ul> 183 * <li><b>Handler Lookup:</b> O(1) average case via ConcurrentHashMap caching</li> 184 * <li><b>Type Registration:</b> Handlers checked in reverse registration order (last wins)</li> 185 * <li><b>Inheritance Support:</b> Handlers support class inheritance and interface implementation</li> 186 * <li><b>Thread Safety:</b> Full concurrency support with no locking overhead after initialization</li> 187 * <li><b>Memory Efficiency:</b> Minimal object allocation during normal operation</li> 188 * </ul> 189 * 190 * <h5 class='section'>Extension Patterns:</h5> 191 * 192 * <p><b>Custom Type Stringification:</b></p> 193 * <p class='bjava'> 194 * <jv>builder</jv>.addStringifier(LocalDateTime.<jk>class</jk>, (<jp>dt</jp>, <jp>conv</jp>) -> 195 * <jp>dt</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE_TIME</jsf>)); 196 * </p> 197 * 198 * <p><b>Custom Collection Handling:</b></p> 199 * <p class='bjava'> 200 * <jv>builder</jv>.addListifier(MyCustomCollection.<jk>class</jk>, (<jp>coll</jp>, <jp>conv</jp>) -> 201 * <jp>coll</jp>.stream().map(<jp>conv</jp>::swap).toList()); 202 * </p> 203 * 204 * <p><b>Custom Object Transformation:</b></p> 205 * <p class='bjava'> 206 * <jv>builder</jv>.addSwapper(LazyValue.<jk>class</jk>, (<jp>lazy</jp>, <jp>conv</jp>) -> 207 * <jp>lazy</jp>.isEvaluated() ? <jp>lazy</jp>.getValue() : "<unevaluated>"); 208 * </p> 209 * 210 * <h5 class='section'>Integration with BCT:</h5> 211 * <p>This class is used internally by all BCT assertion methods in {@link BctAssertions}:</p> 212 * <ul> 213 * <li>{@link BctAssertions#assertBean(Object, String, String)} - Uses getEntry() for property access</li> 214 * <li>{@link BctAssertions#assertList(List, Object...)} - Uses stringify() for element comparison</li> 215 * <li>{@link BctAssertions#assertBeans(Collection, String, String...)} - Uses both getEntry() and stringify()</li> 216 * </ul> 217 * 218 * @see BeanConverter 219 */ 220@SuppressWarnings("rawtypes") 221public class BasicBeanConverter implements BeanConverter { 222 223 /** 224 * Builder for creating customized BasicBeanConverter instances. 225 * 226 * <p>This builder provides a fluent API for configuring custom type handlers, settings, 227 * and property extraction logic. The builder supports registration of four main types 228 * of customizations:</p> 229 * 230 * <h5 class='section'>Type Handlers:</h5> 231 * <ul> 232 * <li><b>{@link #addStringifier(Class, Stringifier)}</b> - Custom string conversion logic</li> 233 * <li><b>{@link #addListifier(Class, Listifier)}</b> - Custom list conversion for collection-like objects</li> 234 * <li><b>{@link #addSwapper(Class, Swapper)}</b> - Pre-processing and object transformation</li> 235 * <li><b>{@link #addPropertyExtractor(PropertyExtractor)}</b> - Custom property access strategies</li> 236 * </ul> 237 * 238 * <h5 class='section'>PropertyExtractors:</h5> 239 * <p>Property extractors define how the converter accesses object properties during nested 240 * field access (e.g., {@code "user.address.city"}). The converter uses a chain-of-responsibility 241 * pattern, trying each registered extractor until one succeeds:</p> 242 * 243 * <ul> 244 * <li><b>{@link PropertyExtractors.ObjectPropertyExtractor}</b> - JavaBean-style properties via reflection</li> 245 * <li><b>{@link PropertyExtractors.ListPropertyExtractor}</b> - Numeric indices and size/length for arrays/collections</li> 246 * <li><b>{@link PropertyExtractors.MapPropertyExtractor}</b> - Key-based access for Map objects</li> 247 * </ul> 248 * 249 * <p>Custom extractors can be added to handle specialized property access patterns:</p> 250 * <p class='bjava'> 251 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 252 * .defaultSettings() 253 * .addPropertyExtractor(<jk>new</jk> MyCustomExtractor()) 254 * .addPropertyExtractor((<jp>obj</jp>, <jp>prop</jp>) -> { 255 * <jk>if</jk> (<jp>obj</jp> <jk>instanceof</jk> MySpecialType <jv>special</jv>) { 256 * <jk>return</jk> <jv>special</jv>.getCustomProperty(<jp>prop</jp>); 257 * } 258 * <jk>return</jk> <jk>null</jk>; <jc>// Try next extractor</jc> 259 * }) 260 * .build(); 261 * </p> 262 * 263 * <h5 class='section'>Default Configuration:</h5> 264 * <p>The {@link #defaultSettings()} method pre-registers comprehensive type handlers 265 * and property extractors for common Java types, providing out-of-the-box functionality 266 * for most use cases while still allowing full customization.</p> 267 * 268 * @see PropertyExtractors 269 * @see PropertyExtractor 270 */ 271 public static class Builder { 272 private Map<String,Object> settings = map(); 273 private List<StringifierEntry<?>> stringifiers = list(); 274 private List<ListifierEntry<?>> listifiers = list(); 275 private List<SizerEntry<?>> sizers = list(); 276 private List<SwapperEntry<?>> swappers = list(); 277 private List<PropertyExtractor> propertyExtractors = list(); 278 279 /** 280 * Registers a custom listifier for a specific type. 281 * 282 * <p>Listifiers convert collection-like objects to List<Object>. The BiFunction 283 * receives the object to convert and the converter instance for recursive calls.</p> 284 * 285 * @param <T> The type to handle 286 * @param c The class to register the listifier for 287 * @param l The listification function 288 * @return This builder for method chaining 289 */ 290 public <T> Builder addListifier(Class<T> c, Listifier<T> l) { 291 listifiers.add(new ListifierEntry<>(c, l)); 292 return this; 293 } 294 295 /** 296 * Registers a custom property extractor for specialized property access logic. 297 * 298 * <p>Property extractors enable custom property access patterns beyond standard JavaBean 299 * conventions. The converter tries extractors in registration order until one returns 300 * a non-<jk>null</jk> value. This allows for:</p> 301 * <ul> 302 * <li><b>Custom data structures:</b> Special property access for non-standard objects</li> 303 * <li><b>Database entities:</b> Property access via entity-specific methods</li> 304 * <li><b>Dynamic properties:</b> Computed or cached property values</li> 305 * <li><b>Legacy objects:</b> Bridging older APIs with modern property access</li> 306 * </ul> 307 * 308 * <h5 class='section'>Implementation Example:</h5> 309 * <p class='bjava'> 310 * <jc>// Custom extractor for a specialized data class</jc> 311 * PropertyExtractor <jv>customExtractor</jv> = (<jp>obj</jp>, <jp>property</jp>) -> { 312 * <jk>if</jk> (<jp>obj</jp> <jk>instanceof</jk> DatabaseEntity <jv>entity</jv>) { 313 * <jk>switch</jk> (<jp>property</jp>) { 314 * <jk>case</jk> <js>"id"</js>: <jk>return</jk> <jv>entity</jv>.getPrimaryKey(); 315 * <jk>case</jk> <js>"displayName"</js>: <jk>return</jk> <jv>entity</jv>.computeDisplayName(); 316 * <jk>case</jk> <js>"metadata"</js>: <jk>return</jk> <jv>entity</jv>.getMetadataAsMap(); 317 * } 318 * } 319 * <jk>return</jk> <jk>null</jk>; <jc>// Let next extractor try</jc> 320 * }; 321 * 322 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 323 * .addPropertyExtractor(<jv>customExtractor</jv>) 324 * .defaultSettings() <jc>// Adds standard extractors</jc> 325 * .build(); 326 * </p> 327 * 328 * <p><b>Execution Order:</b> Custom extractors are tried before the default extractors 329 * added by {@link #defaultSettings()}, allowing overrides of standard behavior.</p> 330 * 331 * @param e The property extractor to register 332 * @return This builder for method chaining 333 * @see PropertyExtractor 334 * @see PropertyExtractors 335 */ 336 public Builder addPropertyExtractor(PropertyExtractor e) { 337 propertyExtractors.add(e); 338 return this; 339 } 340 341 /** 342 * Adds a configuration setting to the converter. 343 * 344 * @param key The setting key (use SETTING_* constants) 345 * @param value The setting value 346 * @return This builder for method chaining 347 */ 348 public Builder addSetting(String key, Object value) { 349 settings.put(key, value); 350 return this; 351 } 352 353 /** 354 * Registers a custom sizer for a specific type. 355 * 356 * <p>Sizers compute the size of collection-like objects for test assertions. 357 * The function receives the object to size and the converter instance for accessing 358 * additional utilities if needed.</p> 359 * 360 * @param <T> The type to handle 361 * @param c The class to register the sizer for 362 * @param s The sizing function 363 * @return This builder for method chaining 364 */ 365 public <T> Builder addSizer(Class<T> c, Sizer<T> s) { 366 sizers.add(new SizerEntry<>(c, s)); 367 return this; 368 } 369 370 /** 371 * Registers a custom stringifier for a specific type. 372 * 373 * <p>Stringifiers convert objects to their string representations. The BiFunction 374 * receives the object to convert and the converter instance for recursive calls.</p> 375 * 376 * @param <T> The type to handle 377 * @param c The class to register the stringifier for 378 * @param s The stringification function 379 * @return This builder for method chaining 380 */ 381 public <T> Builder addStringifier(Class<T> c, Stringifier<T> s) { 382 stringifiers.add(new StringifierEntry<>(c, s)); 383 return this; 384 } 385 386 /** 387 * Registers a custom swapper for a specific type. 388 * 389 * <p>Swappers pre-process objects before conversion. Common uses include 390 * unwrapping Optional values, calling Supplier methods, or extracting values 391 * from wrapper objects.</p> 392 * 393 * @param <T> The type to handle 394 * @param c The class to register the swapper for 395 * @param s The swapping function 396 * @return This builder for method chaining 397 */ 398 public <T> Builder addSwapper(Class<T> c, Swapper<T> s) { 399 swappers.add(new SwapperEntry<>(c, s)); 400 return this; 401 } 402 403 /** 404 * Builds the configured BasicBeanConverter instance. 405 * 406 * <p>This method creates a new BasicBeanConverter with all registered handlers 407 * and settings. The builder can be reused to create multiple converters with 408 * the same configuration.</p> 409 * 410 * @return A new BasicBeanConverter instance 411 */ 412 public BasicBeanConverter build() { 413 return new BasicBeanConverter(this); 414 } 415 416 /** 417 * Adds default handlers and settings for common Java types. 418 * 419 * <p>This method registers comprehensive support for:</p> 420 * <ul> 421 * <li><b>Collections:</b> List, Set, Collection → bracket format</li> 422 * <li><b>Maps:</b> Map, Properties → brace format with key=value pairs</li> 423 * <li><b>Map Entries:</b> Map.Entry → <js>"key=value"</js> format</li> 424 * <li><b>Dates:</b> Date, Calendar → ISO-8601 format</li> 425 * <li><b>Files/Streams:</b> File, InputStream, Reader → content extraction</li> 426 * <li><b>Arrays:</b> byte[], char[] → hex strings and direct string conversion</li> 427 * <li><b>Reflection:</b> Class, Method, Constructor → readable signatures</li> 428 * <li><b>Enums:</b> All enum types → name() format</li> 429 * <li><b>Iterables:</b> Iterable, Iterator, Enumeration, Stream → list conversion</li> 430 * <li><b>Wrappers:</b> Optional, Supplier → unwrapping/evaluation</li> 431 * </ul> 432 * 433 * <p>Default settings configured by this method:</p> 434 * <ul> 435 * <li><code>{@link #SETTING_nullValue}</code> = <js>"<null>"</js> - String representation for null values</li> 436 * <li><code>{@link #SETTING_selfValue}</code> = <js>"<self>"</js> - Special property name for self-reference</li> 437 * <li><code>{@link #SETTING_classNameFormat}</code> = <js>"simple"</js> - Simple class name format</li> 438 * </ul> 439 * 440 * <p>Additional settings that can be configured (not set by default):</p> 441 * <ul> 442 * <li><code>{@link #SETTING_fieldSeparator}</code> - Delimiter between collection elements (default: <js>","</js>)</li> 443 * <li><code>{@link #SETTING_collectionPrefix}</code> - Prefix for collection content (default: <js>"["</js>)</li> 444 * <li><code>{@link #SETTING_collectionSuffix}</code> - Suffix for collection content (default: <js>"]"</js>)</li> 445 * <li><code>{@link #SETTING_mapPrefix}</code> - Prefix for map content (default: <js>"{"</js>)</li> 446 * <li><code>{@link #SETTING_mapSuffix}</code> - Suffix for map content (default: <js>"}"</js>)</li> 447 * <li><code>{@link #SETTING_mapEntrySeparator}</code> - Separator between map keys and values (default: <js>"="</js>)</li> 448 * <li><code>{@link #SETTING_calendarFormat}</code> - DateTimeFormatter for calendar objects (default: <jsf>ISO_INSTANT</jsf>)</li> 449 * </ul> 450 * 451 * <p><b>Note:</b> This should typically be called after custom handlers to avoid 452 * overriding your custom configurations, since handlers are processed in reverse order.</p> 453 * 454 * @return This builder for method chaining 455 */ 456 public Builder defaultSettings() { 457 addSetting(SETTING_nullValue, "<null>"); 458 addSetting(SETTING_selfValue, "<self>"); 459 addSetting(SETTING_classNameFormat, "simple"); 460 461 addStringifier(Map.Entry.class, Stringifiers.mapEntryStringifier()); 462 addStringifier(GregorianCalendar.class, Stringifiers.calendarStringifier()); 463 addStringifier(Date.class, Stringifiers.dateStringifier()); 464 addStringifier(InputStream.class, Stringifiers.inputStreamStringifier()); 465 addStringifier(byte[].class, Stringifiers.byteArrayStringifier()); 466 addStringifier(char[].class, Stringifiers.charArrayStringifier()); 467 addStringifier(Reader.class, Stringifiers.readerStringifier()); 468 addStringifier(File.class, Stringifiers.fileStringifier()); 469 addStringifier(Enum.class, Stringifiers.enumStringifier()); 470 addStringifier(Class.class, Stringifiers.classStringifier()); 471 addStringifier(Constructor.class, Stringifiers.constructorStringifier()); 472 addStringifier(Method.class, Stringifiers.methodStringifier()); 473 addStringifier(List.class, Stringifiers.listStringifier()); 474 addStringifier(Map.class, Stringifiers.mapStringifier()); 475 476 // Note: Listifiers are processed in reverse registration order (last registered wins). 477 // Collection must be registered after Iterable so it takes precedence for Sets, 478 // ensuring TreeSet conversion for deterministic ordering. 479 addListifier(Iterable.class, Listifiers.iterableListifier()); 480 addListifier(Collection.class, Listifiers.collectionListifier()); 481 addListifier(Iterator.class, Listifiers.iteratorListifier()); 482 addListifier(Enumeration.class, Listifiers.enumerationListifier()); 483 addListifier(Stream.class, Listifiers.streamListifier()); 484 addListifier(Map.class, Listifiers.mapListifier()); 485 486 addSwapper(Optional.class, Swappers.optionalSwapper()); 487 addSwapper(Supplier.class, Swappers.supplierSwapper()); 488 addSwapper(Future.class, Swappers.futureSwapper()); 489 490 addPropertyExtractor(new PropertyExtractors.ObjectPropertyExtractor()); 491 addPropertyExtractor(new PropertyExtractors.ListPropertyExtractor()); 492 addPropertyExtractor(new PropertyExtractors.MapPropertyExtractor()); 493 494 return this; 495 } 496 } 497 498 static class ListifierEntry<T> { 499 private Class<T> forClass; 500 private Listifier<T> function; 501 502 private ListifierEntry(Class<T> forClass, Listifier<T> function) { 503 this.forClass = forClass; 504 this.function = function; 505 } 506 } 507 508 static class SizerEntry<T> { 509 private Class<T> forClass; 510 private Sizer<T> function; 511 512 private SizerEntry(Class<T> forClass, Sizer<T> function) { 513 this.forClass = forClass; 514 this.function = function; 515 } 516 } 517 518 static class StringifierEntry<T> { 519 private Class<T> forClass; 520 private Stringifier<T> function; 521 522 @SuppressWarnings("unchecked") 523 private StringifierEntry(Class<T> forClass, Stringifier function) { 524 this.forClass = forClass; 525 this.function = function; 526 } 527 } 528 529 static class SwapperEntry<T> { 530 private Class<T> forClass; 531 private Swapper<T> function; 532 533 private SwapperEntry(Class<T> forClass, Swapper<T> function) { 534 this.forClass = forClass; 535 this.function = function; 536 } 537 } 538 539 /** 540 * Default converter instance with standard settings and handlers. 541 * 542 * <p>This pre-configured instance provides comprehensive support for all common Java types 543 * with sensible default settings. It's suitable for most BCT testing scenarios and can be 544 * used directly without building a custom converter.</p> 545 * 546 * <h5 class='section'>Included Support:</h5> 547 * <ul> 548 * <li><b>Collections:</b> List, Set, Queue with bracket formatting</li> 549 * <li><b>Maps:</b> Map, Properties with brace formatting</li> 550 * <li><b>Arrays:</b> All array types with bracket formatting</li> 551 * <li><b>Dates:</b> Date, Calendar with ISO-8601 formatting</li> 552 * <li><b>Files/Streams:</b> File, InputStream, Reader content extraction</li> 553 * <li><b>Reflection:</b> Class, Method, Constructor readable signatures</li> 554 * <li><b>Enums:</b> name() representation</li> 555 * <li><b>Wrappers:</b> Optional, Supplier, Future unwrapping</li> 556 * </ul> 557 * 558 * <h5 class='section'>Default Settings:</h5> 559 * <ul> 560 * <li><code>nullValue</code> = <js>"<null>"</js></li> 561 * <li><code>selfValue</code> = <js>"<self>"</js></li> 562 * <li><code>classNameFormat</code> = <js>"simple"</js></li> 563 * </ul> 564 * 565 * <h5 class='section'>Usage Example:</h5> 566 * <p class='bjava'> 567 * <jc>// Use default converter for assertions</jc> 568 * <jk>var</jk> <jv>result</jv> = BasicBeanConverter.<jsf>DEFAULT</jsf>.stringify(<jv>myObject</jv>); 569 * <jk>var</jk> <jv>city</jv> = BasicBeanConverter.<jsf>DEFAULT</jsf>.getProperty(<jv>user</jv>, <js>"address.city"</js>); 570 * </p> 571 * 572 * @see #builder() 573 * @see Builder#defaultSettings() 574 */ 575 public static final BasicBeanConverter DEFAULT = builder().defaultSettings().build(); 576 577 /** 578 * Setting key for the string representation of null values. 579 * 580 * <p>Default value: <js>"<null>"</js></p> 581 * 582 * <p>This setting controls how null values are displayed when stringified. 583 * Used by the converter when encountering null objects during conversion.</p> 584 * 585 * @see Builder#addSetting(String, Object) 586 */ 587 public static final String SETTING_nullValue = "nullValue"; 588 589 /** 590 * Setting key for the special property name that returns the object itself. 591 * 592 * <p>Default value: <js>"<self>"</js></p> 593 * 594 * <p>This setting defines the property name that, when accessed, returns the 595 * object itself rather than a property of the object. Useful for self-referential 596 * property access in nested object navigation.</p> 597 * 598 * @see Builder#addSetting(String, Object) 599 */ 600 public static final String SETTING_selfValue = "selfValue"; 601 602 /** 603 * Setting key for the delimiter between collection elements and map entries. 604 * 605 * <p>Default value: <js>","</js></p> 606 * 607 * <p>This setting controls the separator used between elements when converting 608 * collections, arrays, and maps to string representations.</p> 609 * 610 * @see Builder#addSetting(String, Object) 611 */ 612 public static final String SETTING_fieldSeparator = "fieldSeparator"; 613 614 /** 615 * Setting key for the prefix character(s) used around collection content. 616 * 617 * <p>Default value: <js>"["</js></p> 618 * 619 * <p>This setting defines the opening bracket or prefix used when displaying 620 * collection and array contents in string format.</p> 621 * 622 * @see Builder#addSetting(String, Object) 623 * @see #SETTING_collectionSuffix 624 */ 625 public static final String SETTING_collectionPrefix = "collectionPrefix"; 626 627 /** 628 * Setting key for the suffix character(s) used around collection content. 629 * 630 * <p>Default value: <js>"]"</js></p> 631 * 632 * <p>This setting defines the closing bracket or suffix used when displaying 633 * collection and array contents in string format.</p> 634 * 635 * @see Builder#addSetting(String, Object) 636 * @see #SETTING_collectionPrefix 637 */ 638 public static final String SETTING_collectionSuffix = "collectionSuffix"; 639 640 /** 641 * Setting key for the prefix character(s) used around map content. 642 * 643 * <p>Default value: <js>"{"</js></p> 644 * 645 * <p>This setting defines the opening brace or prefix used when displaying 646 * map contents in string format.</p> 647 * 648 * @see Builder#addSetting(String, Object) 649 * @see #SETTING_mapSuffix 650 */ 651 public static final String SETTING_mapPrefix = "mapPrefix"; 652 653 /** 654 * Setting key for the suffix character(s) used around map content. 655 * 656 * <p>Default value: <js>"}"</js></p> 657 * 658 * <p>This setting defines the closing brace or suffix used when displaying 659 * map contents in string format.</p> 660 * 661 * @see Builder#addSetting(String, Object) 662 * @see #SETTING_mapPrefix 663 */ 664 public static final String SETTING_mapSuffix = "mapSuffix"; 665 666 /** 667 * Setting key for the separator between map keys and values. 668 * 669 * <p>Default value: <js>"="</js></p> 670 * 671 * <p>This setting controls the separator used between keys and values when 672 * converting map entries to string representations (e.g., "key=value").</p> 673 * 674 * @see Builder#addSetting(String, Object) 675 */ 676 public static final String SETTING_mapEntrySeparator = "mapEntrySeparator"; 677 678 /** 679 * Setting key for the DateTimeFormatter used for calendar objects. 680 * 681 * <p>Default value: <jsf>ISO_INSTANT</jsf></p> 682 * 683 * <p>This setting defines the date/time format used when converting Calendar 684 * objects to string representations. Must be a valid DateTimeFormatter instance.</p> 685 * 686 * @see Builder#addSetting(String, Object) 687 * @see java.time.format.DateTimeFormatter 688 */ 689 public static final String SETTING_calendarFormat = "calendarFormat"; 690 691 /** 692 * Setting key for the format used when displaying class names. 693 * 694 * <p>Default value: <js>"simple"</js></p> 695 * 696 * <p>This setting controls how class names are displayed in string representations. 697 * Valid values are:</p> 698 * <ul> 699 * <li><js>"simple"</js> - Simple class name only (e.g., "String")</li> 700 * <li><js>"canonical"</js> - Canonical class name (e.g., "java.lang.String")</li> 701 * <li><js>"full"</js> - Full class name with package (e.g., "java.lang.String")</li> 702 * </ul> 703 * 704 * @see Builder#addSetting(String, Object) 705 */ 706 public static final String SETTING_classNameFormat = "classNameFormat"; 707 708 /** 709 * Creates a new builder for configuring a BasicBeanConverter instance. 710 * 711 * <p>The builder allows registration of custom stringifiers, listifiers, and swappers, 712 * as well as configuration of various formatting settings before building the converter.</p> 713 * 714 * @return A new Builder instance 715 */ 716 public static Builder builder() { 717 return new Builder(); 718 } 719 720 private final List<StringifierEntry<?>> stringifiers; 721 private final List<ListifierEntry<?>> listifiers; 722 private final List<SizerEntry<?>> sizers; 723 private final List<SwapperEntry<?>> swappers; 724 725 private final List<PropertyExtractor> propertyExtractors; 726 727 private final Map<String,Object> settings; 728 729 private final ConcurrentHashMap<Class,Optional<Stringifier<?>>> stringifierMap = new ConcurrentHashMap<>(); 730 731 private final ConcurrentHashMap<Class,Optional<Listifier<?>>> listifierMap = new ConcurrentHashMap<>(); 732 733 private final ConcurrentHashMap<Class,Optional<Sizer<?>>> sizerMap = new ConcurrentHashMap<>(); 734 735 private final ConcurrentHashMap<Class,Optional<Swapper<?>>> swapperMap = new ConcurrentHashMap<>(); 736 737 protected BasicBeanConverter(Builder b) { 738 stringifiers = copyOf(b.stringifiers); 739 listifiers = copyOf(b.listifiers); 740 sizers = copyOf(b.sizers); 741 swappers = copyOf(b.swappers); 742 propertyExtractors = copyOf(b.propertyExtractors); 743 settings = copyOf(b.settings); 744 Collections.reverse(stringifiers); 745 Collections.reverse(listifiers); 746 Collections.reverse(swappers); 747 Collections.reverse(propertyExtractors); 748 } 749 750 @Override 751 public boolean canListify(Object o) { 752 o = swap(o); 753 if (o == null) 754 return false; 755 var c = o.getClass(); 756 return o instanceof List || c.isArray() || listifierMap.computeIfAbsent(c, this::findListifier).isPresent(); 757 } 758 759 @Override 760 public String getNested(Object o, NestedTokenizer.Token token) { 761 assertArgNotNull("token", token); 762 763 if (o == null) 764 return getSetting(SETTING_nullValue, null); 765 766 // Handle #{...} syntax for iterating over collections/arrays 767 if (eq("#", token.getValue()) && canListify(o)) { 768 return listify(o).stream().map(x -> token.getNested().stream().map(x2 -> getNested(x, x2)).collect(joining(",", "{", "}"))).collect(joining(",", "[", "]")); 769 } 770 771 // Original logic for regular property access 772 var pn = token.getValue(); 773 var selfValue = getSetting(SETTING_selfValue, "<self>"); 774 775 // Handle special values 776 Object e; 777 if (pn.equals(selfValue)) { 778 e = o; // Return the object itself 779 } else { 780 e = opt(getProperty(o, pn)).orElse(null); 781 } 782 if (e == null || ! token.hasNested()) 783 return stringify(e); 784 return token.getNested().stream().map(x -> getNested(e, x)).map(this::stringify).collect(joining(",", "{", "}")); 785 } 786 787 @Override 788 public Object getProperty(Object object, String name) { 789 var o = swap(object); 790 // @formatter:off 791 return propertyExtractors 792 .stream() 793 .filter(x -> x.canExtract(this, o, name)) 794 .findFirst() 795 .orElseThrow(() -> rex("Could not find extractor for object of type {0}", cn(o))).extract(this, o, name); 796 // @formatter:on 797 } 798 799 @Override 800 @SuppressWarnings("unchecked") 801 public <T> T getSetting(String key, T def) { 802 return (T)settings.getOrDefault(key, def); 803 } 804 805 @Override 806 @SuppressWarnings("unchecked") 807 public List<Object> listify(Object o) { 808 assertArgNotNull("o", o); 809 810 o = swap(o); 811 812 if (o instanceof List) 813 return (List<Object>)o; 814 if (o.getClass().isArray()) 815 return arrayToList(o); 816 817 var c = o.getClass(); 818 var o2 = o; 819 // @formatter:off 820 return listifierMap 821 .computeIfAbsent(c, this::findListifier) 822 .map(x -> (Listifier)x) 823 .map(x -> (List<Object>)x.apply(this, o2)) 824 .orElseThrow(() -> illegalArg("Object of type {0} could not be converted to a list.", cns(o2))); 825 // @formatter:on 826 } 827 828 @Override 829 @SuppressWarnings("unchecked") 830 public int size(Object o) { 831 assertArgNotNull("o", o); 832 833 // Checks for Optional before unpacking. 834 if (o instanceof Optional o2) 835 return o2.isEmpty() ? 0 : 1; 836 837 o = swap(o); 838 839 // Check standard object types. 840 if (o == null) 841 return 0; 842 if (o instanceof Collection o2) 843 return o2.size(); 844 if (o instanceof Map o3) 845 return o3.size(); 846 if (o.getClass().isArray()) 847 return Array.getLength(o); 848 if (o instanceof String o2) 849 return o2.length(); 850 851 // Check for registered custom Sizer 852 var c = o.getClass(); 853 var o2 = o; 854 var sizer = sizerMap.computeIfAbsent(c, this::findSizer); 855 if (sizer.isPresent()) 856 return ((Sizer)sizer.get()).size(o2, this); 857 858 // Try to find size() or length() method via reflection 859 // @formatter:off 860 var sizeResult = info(c).getPublicMethods().stream() 861 .filter(m -> ! m.hasParameters()) 862 .filter(m -> m.hasAnyName("size", "length")) 863 .filter(m -> m.getReturnType().isAny(int.class, Integer.class)) 864 .findFirst() 865 .map(m -> safe(() -> (int) m.invoke(o2))) 866 .filter(Objects::nonNull); 867 // @formatter:on 868 if (sizeResult.isPresent()) 869 return sizeResult.get(); 870 871 // Fall back to listify 872 if (canListify(o)) 873 return listify(o).size(); 874 875 // Try to find isEmpty() method via reflection 876 // @formatter:off 877 var isEmpty = info(o).getPublicMethods().stream() 878 .filter(m -> ! m.hasParameters()) 879 .filter(m -> m.hasName("isEmpty")) 880 .filter(m -> m.getReturnType().isAny(boolean.class, Boolean.class)) 881 .map(m -> safe(() -> (Boolean)m.invoke(o2))) 882 .findFirst(); 883 // @formatter:on 884 if (isEmpty.isPresent()) 885 return isEmpty.get() ? 0 : 1; 886 887 throw illegalArg("Object of type {0} does not have a determinable size.", cns(o)); 888 } 889 890 @Override 891 @SuppressWarnings("unchecked") 892 public String stringify(Object o) { 893 894 o = swap(o); 895 896 if (o == null) 897 return getSetting(SETTING_nullValue, null); 898 899 var c = o.getClass(); 900 var stringifier = stringifierMap.computeIfAbsent(c, this::findStringifier); 901 if (stringifier.isEmpty()) { 902 stringifier = of(canListify(o) ? (bc, o2) -> bc.stringify(bc.listify(o2)) : (bc, o2) -> this.safeToString(o2)); 903 stringifierMap.putIfAbsent(c, stringifier); 904 } 905 var o2 = o; 906 return stringifier.map(x -> (Stringifier)x).map(x -> x.apply(this, o2)).map(this::safeToString).orElse(null); 907 } 908 909 /** 910 * Builder class for configuring BasicBeanConverter instances. 911 * 912 * <p>This builder provides a fluent interface for registering custom type handlers 913 * and configuring conversion settings. All registration methods support method chaining 914 * for convenient configuration.</p> 915 * 916 * <h5 class='section'>Handler Registration:</h5> 917 * <ul> 918 * <li><b>Stringifiers:</b> Custom string conversion logic for specific types</li> 919 * <li><b>Listifiers:</b> Custom list conversion logic for collection-like types</li> 920 * <li><b>Swappers:</b> Pre-processing transformation logic for wrapper types</li> 921 * </ul> 922 * 923 * <h5 class='section'>Registration Order:</h5> 924 * <p>Handlers are checked in reverse registration order (last registered wins). 925 * This allows overriding default handlers by registering more specific ones later.</p> 926 * 927 * <h5 class='section'>Inheritance Support:</h5> 928 * <p>All handlers support class inheritance and interface implementation. 929 * When looking up a handler, the system checks:</p> 930 * <ol> 931 * <li>Exact class match</li> 932 * <li>Interface matches (in order of interface declaration)</li> 933 * <li>Superclass matches (walking up the inheritance hierarchy)</li> 934 * </ol> 935 * 936 * <h5 class='section'>Usage Example:</h5> 937 * <p class='bjava'> 938 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 939 * .defaultSettings() 940 * <jc>// Custom stringification for LocalDateTime</jc> 941 * .addStringifier(LocalDateTime.<jk>class</jk>, (<jp>dt</jp>, <jp>conv</jp>) -> 942 * <jp>dt</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE_TIME</jsf>)) 943 * 944 * <jc>// Custom collection handling for custom type</jc> 945 * .addListifier(MyIterable.<jk>class</jk>, (<jp>iter</jp>, <jp>conv</jp>) -> 946 * <jp>iter</jp>.stream().collect(toList())) 947 * 948 * <jc>// Custom transformation for wrapper type</jc> 949 * .addSwapper(LazyValue.<jk>class</jk>, (<jp>lazy</jp>, <jp>conv</jp>) -> 950 * <jp>lazy</jp>.isComputed() ? <jp>lazy</jp>.get() : <jk>null</jk>) 951 * 952 * <jc>// Configure settings</jc> 953 * .addSetting(<jsf>SETTING_nullValue</jsf>, <js>"<null>"</js>) 954 * .addSetting(<jsf>SETTING_fieldSeparator</jsf>, <js>" | "</js>) 955 * 956 * <jc>// Add default handlers for common types</jc> 957 * .defaultSettings() 958 * .build(); 959 * </p> 960 */ 961 962 @Override 963 @SuppressWarnings("unchecked") 964 public Object swap(Object o) { 965 if (o == null) 966 return null; 967 var c = o.getClass(); 968 var swapper = swapperMap.computeIfAbsent(c, this::findSwapper); 969 if (swapper.isPresent()) 970 return swap(swapper.map(x -> (Swapper)x).map(x -> x.apply(this, o)).orElse(null)); 971 return o; 972 } 973 974 private Optional<Listifier<?>> findListifier(Class<?> c) { 975 if (c == null) 976 return empty(); 977 var l = listifiers.stream().filter(x -> x.forClass.isAssignableFrom(c)).findFirst().orElse(null); 978 if (nn(l)) 979 return of(l.function); 980 return findListifier(c.getSuperclass()); 981 } 982 983 private Optional<Sizer<?>> findSizer(Class<?> c) { 984 if (c == null) 985 return empty(); 986 var s = sizers.stream().filter(x -> x.forClass.isAssignableFrom(c)).findFirst().orElse(null); 987 if (nn(s)) 988 return of(s.function); 989 return findSizer(c.getSuperclass()); 990 } 991 992 private Optional<Stringifier<?>> findStringifier(Class<?> c) { 993 if (c == null) 994 return empty(); 995 var s = stringifiers.stream().filter(x -> x.forClass.isAssignableFrom(c)).findFirst().orElse(null); 996 if (nn(s)) 997 return of(s.function); 998 return findStringifier(c.getSuperclass()); 999 } 1000 1001 private Optional<Swapper<?>> findSwapper(Class<?> c) { 1002 if (c == null) 1003 return empty(); 1004 var s = swappers.stream().filter(x -> x.forClass.isAssignableFrom(c)).findFirst().orElse(null); 1005 if (nn(s)) 1006 return of(s.function); 1007 return findSwapper(c.getSuperclass()); 1008 } 1009 1010 /** 1011 * Safely converts an object to a string, catching any exceptions thrown by toString(). 1012 * 1013 * <p> 1014 * This helper method ensures that exceptions thrown by problematic {@code toString()} implementations 1015 * don't propagate up the call stack. Instead, it returns a descriptive error message containing 1016 * the exception type and message. 1017 * 1018 * @param o The object to convert to a string. May be any object including <jk>null</jk>. 1019 * @return The string representation of the object, or a formatted error message if toString() throws an exception. 1020 * Returns <js>"null"</js> if the object is <jk>null</jk>. 1021 */ 1022 private String safeToString(Object o) { 1023 try { 1024 return o.toString(); 1025 } catch (Throwable t) { // NOSONAR 1026 return cns(t) + ": " + t.getMessage(); 1027 } 1028 } 1029}