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 java.util.Optional.*; 020import static java.util.stream.Collectors.*; 021import static org.apache.juneau.commons.reflect.ReflectionUtils.*; 022import static org.apache.juneau.commons.utils.CollectionUtils.*; 023import static org.apache.juneau.commons.utils.ObjectUtils.*; 024import static org.apache.juneau.commons.utils.Shorts.*; 025import static org.apache.juneau.commons.utils.ThrowableUtils.*; 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({ 221 "java:S115", // Constants use UPPER_snakeCase naming convention 222 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 223 "javabugs:S2259", // False positive - null checks are properly handled via framework patterns 224 "rawtypes", // Raw types necessary for generic type handling 225 "unchecked" // Entries registered per Class<T> are cast back to Listifier/Stringifier/Swapper/Sizer<T> when applied 226}) 227public class BasicBeanConverter implements BeanConverter { 228 229 /** 230 * Builder for creating customized BasicBeanConverter instances. 231 * 232 * <p>This builder provides a fluent API for configuring custom type handlers, settings, 233 * and property extraction logic. The builder supports registration of four main types 234 * of customizations:</p> 235 * 236 * <h5 class='section'>Type Handlers:</h5> 237 * <ul> 238 * <li><b>{@link #addStringifier(Class, Stringifier)}</b> - Custom string conversion logic</li> 239 * <li><b>{@link #addListifier(Class, Listifier)}</b> - Custom list conversion for collection-like objects</li> 240 * <li><b>{@link #addSwapper(Class, Swapper)}</b> - Pre-processing and object transformation</li> 241 * <li><b>{@link #addPropertyExtractor(PropertyExtractor)}</b> - Custom property access strategies</li> 242 * </ul> 243 * 244 * <h5 class='section'>PropertyExtractors:</h5> 245 * <p>Property extractors define how the converter accesses object properties during nested 246 * field access (e.g., {@code "user.address.city"}). The converter uses a chain-of-responsibility 247 * pattern, trying each registered extractor until one succeeds:</p> 248 * 249 * <ul> 250 * <li><b>{@link PropertyExtractors.ObjectPropertyExtractor}</b> - JavaBean-style properties via reflection</li> 251 * <li><b>{@link PropertyExtractors.ListPropertyExtractor}</b> - Numeric indices and size/length for arrays/collections</li> 252 * <li><b>{@link PropertyExtractors.MapPropertyExtractor}</b> - Key-based access for Map objects</li> 253 * </ul> 254 * 255 * <p>Custom extractors can be added to handle specialized property access patterns:</p> 256 * <p class='bjava'> 257 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 258 * .defaultSettings() 259 * .addPropertyExtractor(<jk>new</jk> MyCustomExtractor()) 260 * .addPropertyExtractor((<jp>obj</jp>, <jp>prop</jp>) -> { 261 * <jk>if</jk> (<jp>obj</jp> <jk>instanceof</jk> MySpecialType <jv>special</jv>) { 262 * <jk>return</jk> <jv>special</jv>.getCustomProperty(<jp>prop</jp>); 263 * } 264 * <jk>return</jk> <jk>null</jk>; <jc>// Try next extractor</jc> 265 * }) 266 * .build(); 267 * </p> 268 * 269 * <h5 class='section'>Default Configuration:</h5> 270 * <p>The {@link #defaultSettings()} method pre-registers comprehensive type handlers 271 * and property extractors for common Java types, providing out-of-the-box functionality 272 * for most use cases while still allowing full customization.</p> 273 * 274 * @see PropertyExtractors 275 * @see PropertyExtractor 276 */ 277 public static class Builder { 278 private final Map<String,Object> settings = map(); 279 private final List<StringifierEntry<?>> stringifiers = list(); 280 private final List<ListifierEntry<?>> listifiers = list(); 281 private final List<SizerEntry<?>> sizers = list(); 282 private final List<SwapperEntry<?>> swappers = list(); 283 private final List<PropertyExtractor> propertyExtractors = list(); 284 285 /** 286 * Registers a custom listifier for a specific type. 287 * 288 * <p>Listifiers convert collection-like objects to List<Object>. The BiFunction 289 * receives the object to convert and the converter instance for recursive calls.</p> 290 * 291 * @param <T> The type to handle 292 * @param c The class to register the listifier for 293 * @param l The listification function 294 * @return This builder for method chaining 295 */ 296 public <T> Builder addListifier(Class<T> c, Listifier<T> l) { 297 listifiers.add(new ListifierEntry<>(c, l)); 298 return this; 299 } 300 301 /** 302 * Registers a custom property extractor for specialized property access logic. 303 * 304 * <p>Property extractors enable custom property access patterns beyond standard JavaBean 305 * conventions. The converter tries extractors in registration order until one returns 306 * a non-<jk>null</jk> value. This allows for:</p> 307 * <ul> 308 * <li><b>Custom data structures:</b> Special property access for non-standard objects</li> 309 * <li><b>Database entities:</b> Property access via entity-specific methods</li> 310 * <li><b>Dynamic properties:</b> Computed or cached property values</li> 311 * <li><b>Legacy objects:</b> Bridging older APIs with modern property access</li> 312 * </ul> 313 * 314 * <h5 class='section'>Implementation Example:</h5> 315 * <p class='bjava'> 316 * <jc>// Custom extractor for a specialized data class</jc> 317 * PropertyExtractor <jv>customExtractor</jv> = (<jp>obj</jp>, <jp>property</jp>) -> { 318 * <jk>if</jk> (<jp>obj</jp> <jk>instanceof</jk> DatabaseEntity <jv>entity</jv>) { 319 * <jk>switch</jk> (<jp>property</jp>) { 320 * <jk>case</jk> <js>"id"</js>: <jk>return</jk> <jv>entity</jv>.getPrimaryKey(); 321 * <jk>case</jk> <js>"displayName"</js>: <jk>return</jk> <jv>entity</jv>.computeDisplayName(); 322 * <jk>case</jk> <js>"metadata"</js>: <jk>return</jk> <jv>entity</jv>.getMetadataAsMap(); 323 * } 324 * } 325 * <jk>return</jk> <jk>null</jk>; <jc>// Let next extractor try</jc> 326 * }; 327 * 328 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 329 * .addPropertyExtractor(<jv>customExtractor</jv>) 330 * .defaultSettings() <jc>// Adds standard extractors</jc> 331 * .build(); 332 * </p> 333 * 334 * <p><b>Execution Order:</b> Custom extractors are tried before the default extractors 335 * added by {@link #defaultSettings()}, allowing overrides of standard behavior.</p> 336 * 337 * @param e The property extractor to register 338 * @return This builder for method chaining 339 * @see PropertyExtractor 340 * @see PropertyExtractors 341 */ 342 public Builder addPropertyExtractor(PropertyExtractor e) { 343 propertyExtractors.add(e); 344 return this; 345 } 346 347 /** 348 * Adds a configuration setting to the converter. 349 * 350 * @param key The setting key (use SETTING_* constants) 351 * @param value The setting value 352 * @return This builder for method chaining 353 */ 354 public Builder addSetting(String key, Object value) { 355 settings.put(key, value); 356 return this; 357 } 358 359 /** 360 * Registers a custom sizer for a specific type. 361 * 362 * <p>Sizers compute the size of collection-like objects for test assertions. 363 * The function receives the object to size and the converter instance for accessing 364 * additional utilities if needed.</p> 365 * 366 * @param <T> The type to handle 367 * @param c The class to register the sizer for 368 * @param s The sizing function 369 * @return This builder for method chaining 370 */ 371 public <T> Builder addSizer(Class<T> c, Sizer<T> s) { 372 sizers.add(new SizerEntry<>(c, s)); 373 return this; 374 } 375 376 /** 377 * Registers a custom stringifier for a specific type. 378 * 379 * <p>Stringifiers convert objects to their string representations. The BiFunction 380 * receives the object to convert and the converter instance for recursive calls.</p> 381 * 382 * @param <T> The type to handle 383 * @param c The class to register the stringifier for 384 * @param s The stringification function 385 * @return This builder for method chaining 386 */ 387 public <T> Builder addStringifier(Class<T> c, Stringifier<T> s) { 388 stringifiers.add(new StringifierEntry<>(c, s)); 389 return this; 390 } 391 392 /** 393 * Registers a custom swapper for a specific type. 394 * 395 * <p>Swappers pre-process objects before conversion. Common uses include 396 * unwrapping Optional values, calling Supplier methods, or extracting values 397 * from wrapper objects.</p> 398 * 399 * @param <T> The type to handle 400 * @param c The class to register the swapper for 401 * @param s The swapping function 402 * @return This builder for method chaining 403 */ 404 public <T> Builder addSwapper(Class<T> c, Swapper<T> s) { 405 swappers.add(new SwapperEntry<>(c, s)); 406 return this; 407 } 408 409 /** 410 * Builds the configured BasicBeanConverter instance. 411 * 412 * <p>This method creates a new BasicBeanConverter with all registered handlers 413 * and settings. The builder can be reused to create multiple converters with 414 * the same configuration.</p> 415 * 416 * @return A new BasicBeanConverter instance 417 */ 418 public BasicBeanConverter build() { 419 return new BasicBeanConverter(this); 420 } 421 422 /** 423 * Adds default handlers and settings for common Java types. 424 * 425 * <p>This method registers comprehensive support for:</p> 426 * <ul> 427 * <li><b>Collections:</b> List, Set, Collection → bracket format</li> 428 * <li><b>Maps:</b> Map, Properties → brace format with key=value pairs</li> 429 * <li><b>Map Entries:</b> Map.Entry → <js>"key=value"</js> format</li> 430 * <li><b>Dates:</b> Date, Calendar → ISO-8601 format</li> 431 * <li><b>Files/Streams:</b> File, InputStream, Reader → content extraction</li> 432 * <li><b>Arrays:</b> byte[], char[] → hex strings and direct string conversion</li> 433 * <li><b>Reflection:</b> Class, Method, Constructor → readable signatures</li> 434 * <li><b>Enums:</b> All enum types → name() format</li> 435 * <li><b>Iterables:</b> Iterable, Iterator, Enumeration, Stream → list conversion</li> 436 * <li><b>Wrappers:</b> Optional, Supplier → unwrapping/evaluation</li> 437 * </ul> 438 * 439 * <p>Default settings configured by this method:</p> 440 * <ul> 441 * <li><code>{@link #SETTING_nullValue}</code> = <js>"<null>"</js> - String representation for null values</li> 442 * <li><code>{@link #SETTING_selfValue}</code> = <js>"<self>"</js> - Special property name for self-reference</li> 443 * <li><code>{@link #SETTING_classNameFormat}</code> = <js>"simple"</js> - Simple class name format</li> 444 * </ul> 445 * 446 * <p>Additional settings that can be configured (not set by default):</p> 447 * <ul> 448 * <li><code>{@link #SETTING_fieldSeparator}</code> - Delimiter between collection elements (default: <js>","</js>)</li> 449 * <li><code>{@link #SETTING_collectionPrefix}</code> - Prefix for collection content (default: <js>"["</js>)</li> 450 * <li><code>{@link #SETTING_collectionSuffix}</code> - Suffix for collection content (default: <js>"]"</js>)</li> 451 * <li><code>{@link #SETTING_mapPrefix}</code> - Prefix for map content (default: <js>"{"</js>)</li> 452 * <li><code>{@link #SETTING_mapSuffix}</code> - Suffix for map content (default: <js>"}"</js>)</li> 453 * <li><code>{@link #SETTING_mapEntrySeparator}</code> - Separator between map keys and values (default: <js>"="</js>)</li> 454 * <li><code>{@link #SETTING_calendarFormat}</code> - DateTimeFormatter for calendar objects (default: <jsf>ISO_INSTANT</jsf>)</li> 455 * </ul> 456 * 457 * <p><b>Note:</b> This should typically be called after custom handlers to avoid 458 * overriding your custom configurations, since handlers are processed in reverse order.</p> 459 * 460 * @return This builder for method chaining 461 */ 462 public Builder defaultSettings() { 463 addSetting(SETTING_nullValue, "<null>"); 464 addSetting(SETTING_selfValue, "<self>"); 465 addSetting(SETTING_classNameFormat, "simple"); 466 467 addStringifier(Map.Entry.class, Stringifiers.mapEntryStringifier()); 468 addStringifier(GregorianCalendar.class, Stringifiers.calendarStringifier()); 469 addStringifier(Date.class, Stringifiers.dateStringifier()); 470 addStringifier(InputStream.class, Stringifiers.inputStreamStringifier()); 471 addStringifier(byte[].class, Stringifiers.byteArrayStringifier()); 472 addStringifier(char[].class, Stringifiers.charArrayStringifier()); 473 addStringifier(Reader.class, Stringifiers.readerStringifier()); 474 addStringifier(File.class, Stringifiers.fileStringifier()); 475 addStringifier(Enum.class, Stringifiers.enumStringifier()); 476 addStringifier(Class.class, Stringifiers.classStringifier()); 477 addStringifier(Constructor.class, Stringifiers.constructorStringifier()); 478 addStringifier(Method.class, Stringifiers.methodStringifier()); 479 addStringifier(List.class, Stringifiers.listStringifier()); 480 addStringifier(Map.class, Stringifiers.mapStringifier()); 481 482 // Note: Listifiers are processed in reverse registration order (last registered wins). 483 // Collection must be registered after Iterable so it takes precedence for Sets, 484 // ensuring TreeSet conversion for deterministic ordering. 485 addListifier(Iterable.class, Listifiers.iterableListifier()); 486 addListifier(Collection.class, Listifiers.collectionListifier()); 487 addListifier(Iterator.class, Listifiers.iteratorListifier()); 488 addListifier(Enumeration.class, Listifiers.enumerationListifier()); 489 addListifier(Stream.class, Listifiers.streamListifier()); 490 addListifier(Map.class, Listifiers.mapListifier()); 491 492 addSwapper(Optional.class, Swappers.optionalSwapper()); 493 addSwapper(Supplier.class, Swappers.supplierSwapper()); 494 addSwapper(Future.class, Swappers.futureSwapper()); 495 496 addPropertyExtractor(new PropertyExtractors.ObjectPropertyExtractor()); 497 addPropertyExtractor(new PropertyExtractors.ListPropertyExtractor()); 498 addPropertyExtractor(new PropertyExtractors.MapPropertyExtractor()); 499 500 return this; 501 } 502 } 503 504 static class ListifierEntry<T> { 505 private final Class<T> forClass; 506 private final Listifier<T> function; 507 508 private ListifierEntry(Class<T> forClass, Listifier<T> function) { 509 this.forClass = forClass; 510 this.function = function; 511 } 512 } 513 514 static class SizerEntry<T> { 515 private final Class<T> forClass; 516 private final Sizer<T> function; 517 518 private SizerEntry(Class<T> forClass, Sizer<T> function) { 519 this.forClass = forClass; 520 this.function = function; 521 } 522 } 523 524 static class StringifierEntry<T> { 525 private final Class<T> forClass; 526 private final Stringifier<T> function; 527 528 private StringifierEntry(Class<T> forClass, Stringifier function) { 529 this.forClass = forClass; 530 this.function = function; 531 } 532 } 533 534 static class SwapperEntry<T> { 535 private final Class<T> forClass; 536 private final Swapper<T> function; 537 538 private SwapperEntry(Class<T> forClass, Swapper<T> function) { 539 this.forClass = forClass; 540 this.function = function; 541 } 542 } 543 544 /** 545 * Default converter instance with standard settings and handlers. 546 * 547 * <p>This pre-configured instance provides comprehensive support for all common Java types 548 * with sensible default settings. It's suitable for most BCT testing scenarios and can be 549 * used directly without building a custom converter.</p> 550 * 551 * <h5 class='section'>Included Support:</h5> 552 * <ul> 553 * <li><b>Collections:</b> List, Set, Queue with bracket formatting</li> 554 * <li><b>Maps:</b> Map, Properties with brace formatting</li> 555 * <li><b>Arrays:</b> All array types with bracket formatting</li> 556 * <li><b>Dates:</b> Date, Calendar with ISO-8601 formatting</li> 557 * <li><b>Files/Streams:</b> File, InputStream, Reader content extraction</li> 558 * <li><b>Reflection:</b> Class, Method, Constructor readable signatures</li> 559 * <li><b>Enums:</b> name() representation</li> 560 * <li><b>Wrappers:</b> Optional, Supplier, Future unwrapping</li> 561 * </ul> 562 * 563 * <h5 class='section'>Default Settings:</h5> 564 * <ul> 565 * <li><code>nullValue</code> = <js>"<null>"</js></li> 566 * <li><code>selfValue</code> = <js>"<self>"</js></li> 567 * <li><code>classNameFormat</code> = <js>"simple"</js></li> 568 * </ul> 569 * 570 * <h5 class='section'>Usage Example:</h5> 571 * <p class='bjava'> 572 * <jc>// Use default converter for assertions</jc> 573 * <jk>var</jk> <jv>result</jv> = BasicBeanConverter.<jsf>DEFAULT</jsf>.stringify(<jv>myObject</jv>); 574 * <jk>var</jk> <jv>city</jv> = BasicBeanConverter.<jsf>DEFAULT</jsf>.getProperty(<jv>user</jv>, <js>"address.city"</js>); 575 * </p> 576 * 577 * @see #builder() 578 * @see Builder#defaultSettings() 579 */ 580 public static final BasicBeanConverter DEFAULT = builder().defaultSettings().build(); 581 582 /** 583 * Setting key for the string representation of null values. 584 * 585 * <p>Default value: <js>"<null>"</js></p> 586 * 587 * <p>This setting controls how null values are displayed when stringified. 588 * Used by the converter when encountering null objects during conversion.</p> 589 * 590 * @see Builder#addSetting(String, Object) 591 */ 592 public static final String SETTING_nullValue = "nullValue"; 593 594 /** 595 * Setting key for the special property name that returns the object itself. 596 * 597 * <p>Default value: <js>"<self>"</js></p> 598 * 599 * <p>This setting defines the property name that, when accessed, returns the 600 * object itself rather than a property of the object. Useful for self-referential 601 * property access in nested object navigation.</p> 602 * 603 * @see Builder#addSetting(String, Object) 604 */ 605 public static final String SETTING_selfValue = "selfValue"; 606 607 /** 608 * Setting key for the delimiter between collection elements and map entries. 609 * 610 * <p>Default value: <js>","</js></p> 611 * 612 * <p>This setting controls the separator used between elements when converting 613 * collections, arrays, and maps to string representations.</p> 614 * 615 * @see Builder#addSetting(String, Object) 616 */ 617 public static final String SETTING_fieldSeparator = "fieldSeparator"; 618 619 /** 620 * Setting key for the prefix character(s) used around collection content. 621 * 622 * <p>Default value: <js>"["</js></p> 623 * 624 * <p>This setting defines the opening bracket or prefix used when displaying 625 * collection and array contents in string format.</p> 626 * 627 * @see Builder#addSetting(String, Object) 628 * @see #SETTING_collectionSuffix 629 */ 630 public static final String SETTING_collectionPrefix = "collectionPrefix"; 631 632 /** 633 * Setting key for the suffix character(s) used around collection content. 634 * 635 * <p>Default value: <js>"]"</js></p> 636 * 637 * <p>This setting defines the closing bracket or suffix used when displaying 638 * collection and array contents in string format.</p> 639 * 640 * @see Builder#addSetting(String, Object) 641 * @see #SETTING_collectionPrefix 642 */ 643 public static final String SETTING_collectionSuffix = "collectionSuffix"; 644 645 /** 646 * Setting key for the prefix character(s) used around map content. 647 * 648 * <p>Default value: <js>"{"</js></p> 649 * 650 * <p>This setting defines the opening brace or prefix used when displaying 651 * map contents in string format.</p> 652 * 653 * @see Builder#addSetting(String, Object) 654 * @see #SETTING_mapSuffix 655 */ 656 public static final String SETTING_mapPrefix = "mapPrefix"; 657 658 /** 659 * Setting key for the suffix character(s) used around map content. 660 * 661 * <p>Default value: <js>"}"</js></p> 662 * 663 * <p>This setting defines the closing brace or suffix used when displaying 664 * map contents in string format.</p> 665 * 666 * @see Builder#addSetting(String, Object) 667 * @see #SETTING_mapPrefix 668 */ 669 public static final String SETTING_mapSuffix = "mapSuffix"; 670 671 /** 672 * Setting key for the separator between map keys and values. 673 * 674 * <p>Default value: <js>"="</js></p> 675 * 676 * <p>This setting controls the separator used between keys and values when 677 * converting map entries to string representations (e.g., "key=value").</p> 678 * 679 * @see Builder#addSetting(String, Object) 680 */ 681 public static final String SETTING_mapEntrySeparator = "mapEntrySeparator"; 682 683 /** 684 * Setting key for the DateTimeFormatter used for calendar objects. 685 * 686 * <p>Default value: <jsf>ISO_INSTANT</jsf></p> 687 * 688 * <p>This setting defines the date/time format used when converting Calendar 689 * objects to string representations. Must be a valid DateTimeFormatter instance.</p> 690 * 691 * @see Builder#addSetting(String, Object) 692 * @see java.time.format.DateTimeFormatter 693 */ 694 public static final String SETTING_calendarFormat = "calendarFormat"; 695 696 /** 697 * Setting key for the format used when displaying class names. 698 * 699 * <p>Default value: <js>"simple"</js></p> 700 * 701 * <p>This setting controls how class names are displayed in string representations. 702 * Valid values are:</p> 703 * <ul> 704 * <li><js>"simple"</js> - Simple class name only (e.g., "String")</li> 705 * <li><js>"canonical"</js> - Canonical class name (e.g., "java.lang.String")</li> 706 * <li><js>"full"</js> - Full class name with package (e.g., "java.lang.String")</li> 707 * </ul> 708 * 709 * @see Builder#addSetting(String, Object) 710 */ 711 public static final String SETTING_classNameFormat = "classNameFormat"; 712 713 /** 714 * Creates a new builder for configuring a BasicBeanConverter instance. 715 * 716 * <p>The builder allows registration of custom stringifiers, listifiers, and swappers, 717 * as well as configuration of various formatting settings before building the converter.</p> 718 * 719 * @return A new Builder instance 720 */ 721 public static Builder builder() { 722 return new Builder(); 723 } 724 725 private final List<StringifierEntry<?>> stringifiers; 726 private final List<ListifierEntry<?>> listifiers; 727 private final List<SizerEntry<?>> sizers; 728 private final List<SwapperEntry<?>> swappers; 729 730 private final List<PropertyExtractor> propertyExtractors; 731 732 private final Map<String,Object> settings; 733 734 private final ConcurrentHashMap<Class,Optional<Stringifier<?>>> stringifierMap = new ConcurrentHashMap<>(); 735 736 private final ConcurrentHashMap<Class,Optional<Listifier<?>>> listifierMap = new ConcurrentHashMap<>(); 737 738 private final ConcurrentHashMap<Class,Optional<Sizer<?>>> sizerMap = new ConcurrentHashMap<>(); 739 740 private final ConcurrentHashMap<Class,Optional<Swapper<?>>> swapperMap = new ConcurrentHashMap<>(); 741 742 protected BasicBeanConverter(Builder b) { 743 stringifiers = cp(b.stringifiers); 744 listifiers = cp(b.listifiers); 745 sizers = cp(b.sizers); 746 swappers = cp(b.swappers); 747 propertyExtractors = cp(b.propertyExtractors); 748 settings = cp(b.settings); 749 Collections.reverse(stringifiers); 750 Collections.reverse(listifiers); 751 Collections.reverse(swappers); 752 Collections.reverse(propertyExtractors); 753 } 754 755 @Override 756 public boolean canListify(Object o) { 757 o = swap(o); 758 if (o == null) 759 return false; 760 var c = o.getClass(); 761 return o instanceof List || c.isArray() || listifierMap.computeIfAbsent(c, this::findListifier).isPresent(); 762 } 763 764 @Override 765 public String getNested(Object o, NestedTokenizer.Token token) { 766 reqnn("token", token); 767 768 if (o == null) 769 return getSetting(SETTING_nullValue, null); 770 771 // Handle #{...} syntax for iterating over collections/arrays 772 if (eq("#", token.getValue()) && canListify(o)) { 773 return listify(o).stream().map(x -> token.getNested().stream().map(x2 -> getNested(x, x2)).collect(joining(",", "{", "}"))).collect(joining(",", "[", "]")); 774 } 775 776 // Original logic for regular property access 777 var pn = token.getValue(); 778 var selfValue = getSetting(SETTING_selfValue, "<self>"); 779 780 // Handle special values 781 Object e; 782 if (pn.equals(selfValue)) { 783 e = o; // Return the object itself 784 } else { 785 e = o(getProperty(o, pn)).orElse(null); 786 } 787 if (e == null || ! token.hasNested()) 788 return stringify(e); 789 return token.getNested().stream().map(x -> getNested(e, x)).map(this::stringify).collect(joining(",", "{", "}")); 790 } 791 792 @Override 793 public Object getProperty(Object object, String name) { 794 var o = swap(object); 795 // @formatter:off 796 return propertyExtractors 797 .stream() 798 .filter(x -> x.canExtract(this, o, name)) 799 .findFirst() 800 .orElseThrow(() -> rex("Could not find extractor for object of type %s", cn(o))).extract(this, o, name); 801 // @formatter:on 802 } 803 804 @Override 805 public <T> T getSetting(String key, T def) { 806 return (T)settings.getOrDefault(key, def); 807 } 808 809 @Override 810 public List<Object> listify(Object o) { 811 reqnn("o", o); 812 813 o = swap(o); 814 815 if (o instanceof List) 816 return (List<Object>)o; 817 if (o.getClass().isArray()) 818 return arrayToList(o); 819 820 var c = o.getClass(); 821 var o2 = o; 822 // @formatter:off 823 return listifierMap 824 .computeIfAbsent(c, this::findListifier) 825 .map(x -> (Listifier)x) 826 .map(x -> (List<Object>)x.apply(this, o2)) 827 .orElseThrow(() -> iaex("Object of type %s could not be converted to a list.", cns(o2))); 828 // @formatter:on 829 } 830 831 @Override 832 public int size(Object o) { 833 reqnn("o", o); 834 835 // Checks for Optional before unpacking. 836 if (o instanceof Optional o2) 837 return o2.isEmpty() ? 0 : 1; 838 839 o = swap(o); 840 841 // Check standard object types. 842 if (o == null) 843 return 0; 844 if (o instanceof Collection o2) 845 return o2.size(); 846 if (o instanceof Map o3) 847 return o3.size(); 848 if (o.getClass().isArray()) 849 return Array.getLength(o); 850 if (o instanceof String o2) 851 return o2.length(); 852 853 // Check for registered custom Sizer 854 var c = o.getClass(); 855 var o2 = o; 856 var sizer = sizerMap.computeIfAbsent(c, this::findSizer); 857 if (sizer.isPresent()) 858 return ((Sizer)sizer.get()).size(o2, this); 859 860 // Try to find size() or length() method via reflection 861 // @formatter:off 862 var sizeResult = info(c).getPublicMethods().stream() 863 .filter(m -> ! m.hasParameters()) 864 .filter(m -> m.hasAnyName("size", "length")) 865 .filter(m -> m.getReturnType().isAny(int.class, Integer.class)) 866 .findFirst() 867 .map(m -> safe(() -> (Integer) m.invoke(o2))) 868 .filter(Objects::nonNull); 869 // @formatter:on 870 if (sizeResult.isPresent()) 871 return sizeResult.get(); 872 873 // Fall back to listify 874 if (canListify(o)) 875 return listify(o).size(); 876 877 // Try to find isEmpty() method via reflection 878 // @formatter:off 879 var isEmpty = info(o).getPublicMethods().stream() 880 .filter(m -> ! m.hasParameters()) 881 .filter(m -> m.hasName("isEmpty")) 882 .filter(m -> m.getReturnType().isAny(boolean.class, Boolean.class)) 883 .map(m -> safe(() -> (Boolean)m.invoke(o2))) 884 .findFirst(); 885 // @formatter:on 886 if (isEmpty.isPresent()) 887 return isTrue(isEmpty.get()) ? 0 : 1; 888 889 throw iaex("Object of type %s does not have a determinable size.", cns(o)); 890 } 891 892 @Override 893 public String stringify(Object o) { 894 895 o = swap(o); 896 897 if (o == null) 898 return getSetting(SETTING_nullValue, null); 899 900 var c = o.getClass(); 901 var stringifier = stringifierMap.computeIfAbsent(c, this::findStringifier); 902 if (stringifier.isEmpty()) { 903 stringifier = of(canListify(o) ? (bc, o2) -> bc.stringify(bc.listify(o2)) : (bc, o2) -> this.safeToString(o2)); 904 stringifierMap.putIfAbsent(c, stringifier); 905 } 906 var o2 = o; 907 return stringifier.map(x -> (Stringifier)x).map(x -> x.apply(this, o2)).map(this::safeToString).orElse(null); 908 } 909 910 /** 911 * Builder class for configuring BasicBeanConverter instances. 912 * 913 * <p>This builder provides a fluent interface for registering custom type handlers 914 * and configuring conversion settings. All registration methods support method chaining 915 * for convenient configuration.</p> 916 * 917 * <h5 class='section'>Handler Registration:</h5> 918 * <ul> 919 * <li><b>Stringifiers:</b> Custom string conversion logic for specific types</li> 920 * <li><b>Listifiers:</b> Custom list conversion logic for collection-like types</li> 921 * <li><b>Swappers:</b> Pre-processing transformation logic for wrapper types</li> 922 * </ul> 923 * 924 * <h5 class='section'>Registration Order:</h5> 925 * <p>Handlers are checked in reverse registration order (last registered wins). 926 * This allows overriding default handlers by registering more specific ones later.</p> 927 * 928 * <h5 class='section'>Inheritance Support:</h5> 929 * <p>All handlers support class inheritance and interface implementation. 930 * When looking up a handler, the system checks:</p> 931 * <ol> 932 * <li>Exact class match</li> 933 * <li>Interface matches (in order of interface declaration)</li> 934 * <li>Superclass matches (walking up the inheritance hierarchy)</li> 935 * </ol> 936 * 937 * <h5 class='section'>Usage Example:</h5> 938 * <p class='bjava'> 939 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 940 * .defaultSettings() 941 * <jc>// Custom stringification for LocalDateTime</jc> 942 * .addStringifier(LocalDateTime.<jk>class</jk>, (<jp>dt</jp>, <jp>conv</jp>) -> 943 * <jp>dt</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE_TIME</jsf>)) 944 * 945 * <jc>// Custom collection handling for custom type</jc> 946 * .addListifier(MyIterable.<jk>class</jk>, (<jp>iter</jp>, <jp>conv</jp>) -> 947 * <jp>iter</jp>.stream().collect(toList())) 948 * 949 * <jc>// Custom transformation for wrapper type</jc> 950 * .addSwapper(LazyValue.<jk>class</jk>, (<jp>lazy</jp>, <jp>conv</jp>) -> 951 * <jp>lazy</jp>.isComputed() ? <jp>lazy</jp>.get() : <jk>null</jk>) 952 * 953 * <jc>// Configure settings</jc> 954 * .addSetting(<jsf>SETTING_nullValue</jsf>, <js>"<null>"</js>) 955 * .addSetting(<jsf>SETTING_fieldSeparator</jsf>, <js>" | "</js>) 956 * 957 * <jc>// Add default handlers for common types</jc> 958 * .defaultSettings() 959 * .build(); 960 * </p> 961 */ 962 963 @Override 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 (Exception t) { 1026 return cns(t) + ": " + t.getMessage(); 1027 } 1028 } 1029}