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 org.apache.juneau.junit.bct.Utils.*; 021import static java.util.stream.Collectors.*; 022 023import java.io.*; 024import java.lang.reflect.*; 025import java.util.*; 026import java.util.concurrent.*; 027import java.util.function.*; 028import java.util.stream.*; 029 030/** 031 * Default implementation of {@link BeanConverter} for Bean-Centric Test (BCT) object conversion. 032 * 033 * <p>This class provides a comprehensive, extensible framework for converting Java objects to strings 034 * and lists, with sophisticated property access capabilities. It's the core engine behind BCT testing 035 * assertions, handling complex object introspection and value extraction with high performance through 036 * intelligent caching and optimized lookup strategies.</p> 037 * 038 * <h5 class='section'>Key Features:</h5> 039 * <ul> 040 * <li><b>Extensible Type Handlers:</b> Pluggable stringifiers, listifiers, and swappers for custom types</li> 041 * <li><b>Performance Optimization:</b> ConcurrentHashMap caching for type-to-handler mappings</li> 042 * <li><b>Comprehensive Defaults:</b> Built-in support for all common Java types and structures</li> 043 * <li><b>Configurable Settings:</b> Customizable formatting, delimiters, and display options</li> 044 * <li><b>Thread Safety:</b> Fully thread-safe implementation suitable for concurrent testing</li> 045 * </ul> 046 * 047 * <h5 class='section'>Architecture Overview:</h5> 048 * <p>The converter uses four types of pluggable handlers:</p> 049 * <dl> 050 * <dt><b>Stringifiers:</b></dt> 051 * <dd>Convert objects to string representations with custom formatting rules</dd> 052 * 053 * <dt><b>Listifiers:</b></dt> 054 * <dd>Convert collection-like objects to List<Object> for uniform iteration</dd> 055 * 056 * <dt><b>Swappers:</b></dt> 057 * <dd>Pre-process objects before conversion (unwrap Optional, call Supplier, etc.)</dd> 058 * 059 * <dt><b>PropertyExtractors:</b></dt> 060 * <dd>Define custom property access strategies for nested field navigation (e.g., <js>"user.address.city"</js>)</dd> 061 * </dl> 062 * 063 * <p>PropertyExtractors use a chain-of-responsibility pattern, where each extractor in the chain 064 * is tried until one can handle the property access. The framework includes built-in extractors for:</p> 065 * <ul> 066 * <li><b>JavaBean properties:</b> Standard getter methods and public fields</li> 067 * <li><b>Collection/Array access:</b> Numeric indices and size/length properties</li> 068 * <li><b>Map access:</b> Key-based property retrieval and size property</li> 069 * </ul> 070 * 071 * <h5 class='section'>Default Type Support:</h5> 072 * <p>Out-of-the-box stringification support includes:</p> 073 * <ul> 074 * <li><b>Collections:</b> List, Set, Queue → <js>"[item1,item2,item3]"</js> format</li> 075 * <li><b>Maps:</b> Map, Properties → <js>"{key1=value1,key2=value2}"</js> format</li> 076 * <li><b>Map Entries:</b> Map.Entry → <js>"key=value"</js> format</li> 077 * <li><b>Arrays:</b> All array types → <js>"[element1,element2]"</js> format</li> 078 * <li><b>Dates:</b> Date, Calendar → ISO-8601 format</li> 079 * <li><b>Files/Streams:</b> File, InputStream, Reader → content as hex or text</li> 080 * <li><b>Reflection:</b> Class, Method, Constructor → human-readable signatures</li> 081 * <li><b>Enums:</b> Enum values → name() format</li> 082 * </ul> 083 * 084 * <p>Default listification support includes:</p> 085 * <ul> 086 * <li><b>Collection types:</b> List, Set, Queue, and all subtypes</li> 087 * <li><b>Iterable objects:</b> Any Iterable implementation</li> 088 * <li><b>Iterators:</b> Iterator and Enumeration (consumed to list)</li> 089 * <li><b>Streams:</b> Stream objects (terminated to list)</li> 090 * <li><b>Optional:</b> Empty list or single-element list</li> 091 * <li><b>Maps:</b> Converted to list of Map.Entry objects</li> 092 * </ul> 093 * 094 * <p>Default swapping support includes:</p> 095 * <ul> 096 * <li><b>Optional:</b> Unwrapped to contained value or <jk>null</jk></li> 097 * <li><b>Supplier:</b> Called to get supplied value</li> 098 * <li><b>Future:</b> Extracts completed result or returns <js>"<pending>"</js> for incomplete futures (via {@link Swappers#futureSwapper()})</li> 099 * </ul> 100 * 101 * <h5 class='section'>Configuration Settings:</h5> 102 * <p>The converter supports extensive customization via settings:</p> 103 * <dl> 104 * <dt><code>nullValue</code></dt> 105 * <dd>String representation for null values (default: <js>"<null>"</js>)</dd> 106 * 107 * <dt><code>selfValue</code></dt> 108 * <dd>Special property name that returns the object itself (default: <js>"<self>"</js>)</dd> 109 * 110 * <dt><code>emptyValue</code></dt> 111 * <dd>String representation for empty collections (default: <js>"<empty>"</js>)</dd> 112 * 113 * <dt><code>fieldSeparator</code></dt> 114 * <dd>Delimiter between collection elements and map entries (default: <js>","</js>)</dd> 115 * 116 * <dt><code>collectionPrefix/Suffix</code></dt> 117 * <dd>Brackets around collection content (default: <js>"["</js> and <js>"]"</js>)</dd> 118 * 119 * <dt><code>mapPrefix/Suffix</code></dt> 120 * <dd>Brackets around map content (default: <js>"{"</js> and <js>"}"</js>)</dd> 121 * 122 * <dt><code>mapEntrySeparator</code></dt> 123 * <dd>Separator between map keys and values (default: <js>"="</js>)</dd> 124 * 125 * <dt><code>calendarFormat</code></dt> 126 * <dd>DateTimeFormatter for calendar objects (default: <jsf>ISO_INSTANT</jsf>)</dd> 127 * 128 * <dt><code>classNameFormat</code></dt> 129 * <dd>Format for class names: <js>"simple"</js>, <js>"canonical"</js>, or <js>"full"</js> (default: <js>"simple"</js>)</dd> 130 * </dl> 131 * 132 * <h5 class='section'>Usage Examples:</h5> 133 * 134 * <p><b>Basic Usage with Defaults:</b></p> 135 * <p class='bjava'> 136 * <jc>// Use default converter</jc> 137 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsf>DEFAULT</jsf>; 138 * <jk>var</jk> <jv>result</jv> = <jv>converter</jv>.stringify(<jv>myObject</jv>); 139 * </p> 140 * 141 * <p><b>Custom Configuration:</b></p> 142 * <p class='bjava'> 143 * <jc>// Build custom converter</jc> 144 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 145 * .defaultSettings() 146 * .addSetting(<jsf>SETTING_nullValue</jsf>, <js>"<null>"</js>) 147 * .addSetting(<jsf>SETTING_fieldSeparator</jsf>, <js>" | "</js>) 148 * .addStringifier(MyClass.<jk>class</jk>, (<jp>obj</jp>, <jp>conv</jp>) -> <js>"MyClass["</js> + <jp>obj</jp>.getName() + <js>"]"</js>) 149 * .addListifier(MyIterable.<jk>class</jk>, (<jp>obj</jp>, <jp>conv</jp>) -> <jp>obj</jp>.toList()) 150 * .addSwapper(MyWrapper.<jk>class</jk>, (<jp>obj</jp>, <jp>conv</jp>) -> <jp>obj</jp>.getWrapped()) 151 * .build(); 152 * </p> 153 * 154 * <p><b>Complex Property Access:</b></p> 155 * <p class='bjava'> 156 * <jc>// Extract nested properties</jc> 157 * <jk>var</jk> <jv>name</jv> = <jv>converter</jv>.getEntry(<jv>user</jv>, <js>"name"</js>); 158 * <jk>var</jk> <jv>city</jv> = <jv>converter</jv>.getEntry(<jv>user</jv>, <js>"address.city"</js>); 159 * <jk>var</jk> <jv>firstOrder</jv> = <jv>converter</jv>.getEntry(<jv>user</jv>, <js>"orders.0.id"</js>); 160 * <jk>var</jk> <jv>orderCount</jv> = <jv>converter</jv>.getEntry(<jv>user</jv>, <js>"orders.length"</js>); 161 * </p> 162 * 163 * <p><b>Special Property Values:</b></p> 164 * <p class='bjava'> 165 * <jc>// Use special property names</jc> 166 * <jk>var</jk> <jv>userObj</jv> = <jv>converter</jv>.getEntry(<jv>user</jv>, <js>"<self>"</js>); <jc>// Returns the user object itself</jc> 167 * <jk>var</jk> <jv>nullValue</jv> = <jv>converter</jv>.getEntry(<jv>user</jv>, <js>"<null>"</js>); <jc>// Returns null</jc> 168 * 169 * <jc>// Custom self value</jc> 170 * <jk>var</jk> <jv>customConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 171 * .defaultSettings() 172 * .addSetting(<jsf>SETTING_selfValue</jsf>, <js>"this"</js>) 173 * .build(); 174 * <jk>var</jk> <jv>selfRef</jv> = <jv>customConverter</jv>.getEntry(<jv>user</jv>, <js>"this"</js>); <jc>// Returns user object</jc> 175 * </p> 176 * 177 * <h5 class='section'>Performance Characteristics:</h5> 178 * <ul> 179 * <li><b>Handler Lookup:</b> O(1) average case via ConcurrentHashMap caching</li> 180 * <li><b>Type Registration:</b> Handlers checked in reverse registration order (last wins)</li> 181 * <li><b>Inheritance Support:</b> Handlers support class inheritance and interface implementation</li> 182 * <li><b>Thread Safety:</b> Full concurrency support with no locking overhead after initialization</li> 183 * <li><b>Memory Efficiency:</b> Minimal object allocation during normal operation</li> 184 * </ul> 185 * 186 * <h5 class='section'>Extension Patterns:</h5> 187 * 188 * <p><b>Custom Type Stringification:</b></p> 189 * <p class='bjava'> 190 * <jv>builder</jv>.addStringifier(LocalDateTime.<jk>class</jk>, (<jp>dt</jp>, <jp>conv</jp>) -> 191 * <jp>dt</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE_TIME</jsf>)); 192 * </p> 193 * 194 * <p><b>Custom Collection Handling:</b></p> 195 * <p class='bjava'> 196 * <jv>builder</jv>.addListifier(MyCustomCollection.<jk>class</jk>, (<jp>coll</jp>, <jp>conv</jp>) -> 197 * <jp>coll</jp>.stream().map(<jp>conv</jp>::swap).toList()); 198 * </p> 199 * 200 * <p><b>Custom Object Transformation:</b></p> 201 * <p class='bjava'> 202 * <jv>builder</jv>.addSwapper(LazyValue.<jk>class</jk>, (<jp>lazy</jp>, <jp>conv</jp>) -> 203 * <jp>lazy</jp>.isEvaluated() ? <jp>lazy</jp>.getValue() : "<unevaluated>"); 204 * </p> 205 * 206 * <h5 class='section'>Integration with BCT:</h5> 207 * <p>This class is used internally by all BCT assertion methods in {@link BctAssertions}:</p> 208 * <ul> 209 * <li>{@link BctAssertions#assertBean(Object, String, String)} - Uses getEntry() for property access</li> 210 * <li>{@link BctAssertions#assertList(List, Object...)} - Uses stringify() for element comparison</li> 211 * <li>{@link BctAssertions#assertBeans(Collection, String, String...)} - Uses both getEntry() and stringify()</li> 212 * </ul> 213 * 214 * @see BeanConverter 215 */ 216@SuppressWarnings("rawtypes") 217public class BasicBeanConverter implements BeanConverter { 218 219 public static final BasicBeanConverter DEFAULT = builder().defaultSettings().build(); 220 221 public static final String 222 SETTING_nullValue = "nullValue", 223 SETTING_selfValue = "selfValue", 224 SETTING_fieldSeparator = "fieldSeparator", 225 SETTING_collectionPrefix = "collectionPrefix", 226 SETTING_collectionSuffix = "collectionSuffix", 227 SETTING_mapPrefix = "mapPrefix", 228 SETTING_mapSuffix = "mapSuffix", 229 SETTING_mapEntrySeparator = "mapEntrySeparator", 230 SETTING_calendarFormat = "calendarFormat", 231 SETTING_classNameFormat = "classNameFormat"; 232 233 private final List<StringifierEntry<?>> stringifiers; 234 private final List<ListifierEntry<?>> listifiers; 235 private final List<SwapperEntry<?>> swappers; 236 private final List<PropertyExtractor> propertyExtractors; 237 private final Map<String,Object> settings; 238 239 private final ConcurrentHashMap<Class,Optional<Stringifier<?>>> stringifierMap = new ConcurrentHashMap<>(); 240 private final ConcurrentHashMap<Class,Optional<Listifier<?>>> listifierMap = new ConcurrentHashMap<>(); 241 private final ConcurrentHashMap<Class,Optional<Swapper<?>>> swapperMap = new ConcurrentHashMap<>(); 242 243 protected BasicBeanConverter(Builder b) { 244 stringifiers = new ArrayList<>(b.stringifiers); 245 listifiers = new ArrayList<>(b.listifiers); 246 swappers = new ArrayList<>(b.swappers); 247 propertyExtractors = new ArrayList<>(b.propertyExtractors); 248 settings = new HashMap<>(b.settings); 249 Collections.reverse(stringifiers); 250 Collections.reverse(listifiers); 251 Collections.reverse(swappers); 252 Collections.reverse(propertyExtractors); 253 } 254 255 /** 256 * Creates a new builder for configuring a BasicBeanConverter instance. 257 * 258 * <p>The builder allows registration of custom stringifiers, listifiers, and swappers, 259 * as well as configuration of various formatting settings before building the converter.</p> 260 * 261 * @return A new Builder instance 262 */ 263 public static Builder builder() { 264 return new Builder(); 265 } 266 267 @Override 268 public String stringify(Object o) { 269 o = swap(o); 270 if (o == null) 271 return getSetting(SETTING_nullValue, null); 272 var c = o.getClass(); 273 var stringifier = stringifierMap.computeIfAbsent(c, this::findStringifier); 274 if (stringifier.isEmpty()) { 275 stringifier = of(canListify(o) ? (bc, o2) -> bc.stringify(bc.listify(o2)) : (bc, o2) -> safeToString(o2)); 276 stringifierMap.putIfAbsent(c, stringifier); 277 } 278 var o2 = o; 279 return stringifier.map(x -> (Stringifier)x).map(x -> x.apply(this, o2)).map(Utils::safeToString).orElse(null); 280 } 281 282 @Override 283 public Object swap(Object o) { 284 if (o == null) return null; 285 var c = o.getClass(); 286 var swapper = swapperMap.computeIfAbsent(c, this::findSwapper); 287 if (swapper.isPresent()) 288 return swap(swapper.map(x -> (Swapper)x).map(x -> x.apply(this, o)).orElse(null)); 289 return o; 290 } 291 292 @Override 293 public List<Object> listify(Object o) { 294 assertArgNotNull("o", o); 295 o = swap(o); 296 if (o instanceof List) 297 return (List<Object>)o; 298 var c = o.getClass(); 299 if (c.isArray()) 300 return arrayToList(o); 301 var o2 = o; 302 return listifierMap.computeIfAbsent(c, this::findListifier).map(x -> (Listifier)x).map(x -> (List<Object>)x.apply(this, o2)).orElseThrow(()->new IllegalArgumentException(f("Object of type {0} could not be converted to a list.", t(o2)))); 303 } 304 305 @Override 306 public boolean canListify(Object o) { 307 o = swap(o); 308 if (o == null) 309 return false; 310 var c = o.getClass(); 311 return o instanceof List || c.isArray() || listifierMap.computeIfAbsent(c, this::findListifier).isPresent(); 312 } 313 314 @Override 315 public Object getProperty(Object object, String name) { 316 var o = swap(object); 317 return propertyExtractors 318 .stream() 319 .filter(x -> x.canExtract(this, o, name)) 320 .findFirst() 321 .orElseThrow(()->new RuntimeException(f("Could not find extractor for object of type {0}", o.getClass().getName()))) 322 .extract(this, o, name); 323 } 324 325 @Override 326 public String getNested(Object o, NestedTokenizer.Token token) { 327 assertArgNotNull("token", token); 328 329 if (o == null) return getSetting(SETTING_nullValue, null); 330 331 // Handle #{...} syntax for iterating over collections/arrays 332 if (eq("#", token.getValue()) && canListify(o)) { 333 return listify(o).stream().map(x -> token.getNested().stream().map(x2 -> getNested(x, x2)).collect(joining(",","{","}"))).collect(joining(",","[","]")); 334 } 335 336 // Original logic for regular property access 337 var pn = token.getValue(); 338 var selfValue = getSetting(SETTING_selfValue, "<self>"); 339 340 // Handle special values 341 Object e; 342 if (pn.equals(selfValue)) { 343 e = o; // Return the object itself 344 } else { 345 e = ofNullable(getProperty(o, pn)).orElse(null); 346 } 347 if (e == null || ! token.hasNested()) return stringify(e); 348 return token.getNested().stream().map(x -> getNested(e, x)).map(this::stringify).collect(joining(",","{","}")); 349 } 350 351 private Optional<Stringifier<?>> findStringifier(Class<?> c) { 352 if (c == null) 353 return empty(); 354 var s = stringifiers.stream().filter(x -> x.forClass.isAssignableFrom(c)).findFirst().orElse(null); 355 if (s != null) 356 return of(s.function); 357 return findStringifier(c.getSuperclass()); 358 } 359 360 private Optional<Listifier<?>> findListifier(Class<?> c) { 361 if (c == null) 362 return empty(); 363 var l = listifiers.stream().filter(x -> x.forClass.isAssignableFrom(c)).findFirst().orElse(null); 364 if (l != null) 365 return of(l.function); 366 return findListifier(c.getSuperclass()); 367 } 368 369 private Optional<Swapper<?>> findSwapper(Class<?> c) { 370 if (c == null) 371 return empty(); 372 var s = swappers.stream().filter(x -> x.forClass.isAssignableFrom(c)).findFirst().orElse(null); 373 if (s != null) 374 return of(s.function); 375 return findSwapper(c.getSuperclass()); 376 } 377 378 @Override 379 public <T> T getSetting(String key, T def) { 380 return (T)settings.getOrDefault(key, def); 381 } 382 383 /** 384 * Builder class for configuring BasicBeanConverter instances. 385 * 386 * <p>This builder provides a fluent interface for registering custom type handlers 387 * and configuring conversion settings. All registration methods support method chaining 388 * for convenient configuration.</p> 389 * 390 * <h5 class='section'>Handler Registration:</h5> 391 * <ul> 392 * <li><b>Stringifiers:</b> Custom string conversion logic for specific types</li> 393 * <li><b>Listifiers:</b> Custom list conversion logic for collection-like types</li> 394 * <li><b>Swappers:</b> Pre-processing transformation logic for wrapper types</li> 395 * </ul> 396 * 397 * <h5 class='section'>Registration Order:</h5> 398 * <p>Handlers are checked in reverse registration order (last registered wins). 399 * This allows overriding default handlers by registering more specific ones later.</p> 400 * 401 * <h5 class='section'>Inheritance Support:</h5> 402 * <p>All handlers support class inheritance and interface implementation. 403 * When looking up a handler, the system checks:</p> 404 * <ol> 405 * <li>Exact class match</li> 406 * <li>Interface matches (in order of interface declaration)</li> 407 * <li>Superclass matches (walking up the inheritance hierarchy)</li> 408 * </ol> 409 * 410 * <h5 class='section'>Usage Example:</h5> 411 * <p class='bjava'> 412 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 413 * .defaultSettings() 414 * <jc>// Custom stringification for LocalDateTime</jc> 415 * .addStringifier(LocalDateTime.<jk>class</jk>, (<jp>dt</jp>, <jp>conv</jp>) -> 416 * <jp>dt</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE_TIME</jsf>)) 417 * 418 * <jc>// Custom collection handling for custom type</jc> 419 * .addListifier(MyIterable.<jk>class</jk>, (<jp>iter</jp>, <jp>conv</jp>) -> 420 * <jp>iter</jp>.stream().collect(toList())) 421 * 422 * <jc>// Custom transformation for wrapper type</jc> 423 * .addSwapper(LazyValue.<jk>class</jk>, (<jp>lazy</jp>, <jp>conv</jp>) -> 424 * <jp>lazy</jp>.isComputed() ? <jp>lazy</jp>.get() : <jk>null</jk>) 425 * 426 * <jc>// Configure settings</jc> 427 * .addSetting(<jsf>SETTING_nullValue</jsf>, <js>"<null>"</js>) 428 * .addSetting(<jsf>SETTING_fieldSeparator</jsf>, <js>" | "</js>) 429 * 430 * <jc>// Add default handlers for common types</jc> 431 * .defaultSettings() 432 * .build(); 433 * </p> 434 */ 435 436 /** 437 * Builder for creating customized BasicBeanConverter instances. 438 * 439 * <p>This builder provides a fluent API for configuring custom type handlers, settings, 440 * and property extraction logic. The builder supports registration of four main types 441 * of customizations:</p> 442 * 443 * <h5 class='section'>Type Handlers:</h5> 444 * <ul> 445 * <li><b>{@link #addStringifier(Class, Stringifier)}</b> - Custom string conversion logic</li> 446 * <li><b>{@link #addListifier(Class, Listifier)}</b> - Custom list conversion for collection-like objects</li> 447 * <li><b>{@link #addSwapper(Class, Swapper)}</b> - Pre-processing and object transformation</li> 448 * <li><b>{@link #addPropertyExtractor(PropertyExtractor)}</b> - Custom property access strategies</li> 449 * </ul> 450 * 451 * <h5 class='section'>PropertyExtractors:</h5> 452 * <p>Property extractors define how the converter accesses object properties during nested 453 * field access (e.g., {@code "user.address.city"}). The converter uses a chain-of-responsibility 454 * pattern, trying each registered extractor until one succeeds:</p> 455 * 456 * <ul> 457 * <li><b>{@link PropertyExtractors.ObjectPropertyExtractor}</b> - JavaBean-style properties via reflection</li> 458 * <li><b>{@link PropertyExtractors.ListPropertyExtractor}</b> - Numeric indices and size/length for arrays/collections</li> 459 * <li><b>{@link PropertyExtractors.MapPropertyExtractor}</b> - Key-based access for Map objects</li> 460 * </ul> 461 * 462 * <p>Custom extractors can be added to handle specialized property access patterns:</p> 463 * <p class='bjava'> 464 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 465 * .defaultSettings() 466 * .addPropertyExtractor(<jk>new</jk> MyCustomExtractor()) 467 * .addPropertyExtractor((<jp>obj</jp>, <jp>prop</jp>) -> { 468 * <jk>if</jk> (<jp>obj</jp> <jk>instanceof</jk> MySpecialType <jv>special</jv>) { 469 * <jk>return</jk> <jv>special</jv>.getCustomProperty(<jp>prop</jp>); 470 * } 471 * <jk>return</jk> <jk>null</jk>; <jc>// Try next extractor</jc> 472 * }) 473 * .build(); 474 * </p> 475 * 476 * <h5 class='section'>Default Configuration:</h5> 477 * <p>The {@link #defaultSettings()} method pre-registers comprehensive type handlers 478 * and property extractors for common Java types, providing out-of-the-box functionality 479 * for most use cases while still allowing full customization.</p> 480 * 481 * @see PropertyExtractors 482 * @see PropertyExtractor 483 */ 484 public static class Builder { 485 private Map<String,Object> settings = new HashMap<>(); 486 private List<StringifierEntry<?>> stringifiers = new ArrayList<>(); 487 private List<ListifierEntry<?>> listifiers = new ArrayList<>(); 488 private List<SwapperEntry<?>> swappers = new ArrayList<>(); 489 private List<PropertyExtractor> propertyExtractors = new ArrayList<>(); 490 491 /** 492 * Adds a configuration setting to the converter. 493 * 494 * @param key The setting key (use SETTING_* constants) 495 * @param value The setting value 496 * @return This builder for method chaining 497 */ 498 public Builder addSetting(String key, Object value) { settings.put(key, value); return this; } 499 500 /** 501 * Registers a custom stringifier for a specific type. 502 * 503 * <p>Stringifiers convert objects to their string representations. The BiFunction 504 * receives the object to convert and the converter instance for recursive calls.</p> 505 * 506 * @param <T> The type to handle 507 * @param c The class to register the stringifier for 508 * @param s The stringification function 509 * @return This builder for method chaining 510 */ 511 public <T> Builder addStringifier(Class<T> c, Stringifier<T> s) { stringifiers.add(new StringifierEntry<>(c, s)); return this; } 512 513 /** 514 * Registers a custom listifier for a specific type. 515 * 516 * <p>Listifiers convert collection-like objects to List<Object>. The BiFunction 517 * receives the object to convert and the converter instance for recursive calls.</p> 518 * 519 * @param <T> The type to handle 520 * @param c The class to register the listifier for 521 * @param l The listification function 522 * @return This builder for method chaining 523 */ 524 public <T> Builder addListifier(Class<T> c, Listifier<T> l) { listifiers.add(new ListifierEntry<>(c, l)); return this; } 525 526 /** 527 * Registers a custom swapper for a specific type. 528 * 529 * <p>Swappers pre-process objects before conversion. Common uses include 530 * unwrapping Optional values, calling Supplier methods, or extracting values 531 * from wrapper objects.</p> 532 * 533 * @param <T> The type to handle 534 * @param c The class to register the swapper for 535 * @param s The swapping function 536 * @return This builder for method chaining 537 */ 538 public <T> Builder addSwapper(Class<T> c, Swapper<T> s) { swappers.add(new SwapperEntry<>(c, s)); return this; } 539 540 /** 541 * Registers a custom property extractor for specialized property access logic. 542 * 543 * <p>Property extractors enable custom property access patterns beyond standard JavaBean 544 * conventions. The converter tries extractors in registration order until one returns 545 * a non-<jk>null</jk> value. This allows for:</p> 546 * <ul> 547 * <li><b>Custom data structures:</b> Special property access for non-standard objects</li> 548 * <li><b>Database entities:</b> Property access via entity-specific methods</li> 549 * <li><b>Dynamic properties:</b> Computed or cached property values</li> 550 * <li><b>Legacy objects:</b> Bridging older APIs with modern property access</li> 551 * </ul> 552 * 553 * <h5 class='section'>Implementation Example:</h5> 554 * <p class='bjava'> 555 * <jc>// Custom extractor for a specialized data class</jc> 556 * PropertyExtractor <jv>customExtractor</jv> = (<jp>obj</jp>, <jp>property</jp>) -> { 557 * <jk>if</jk> (<jp>obj</jp> <jk>instanceof</jk> DatabaseEntity <jv>entity</jv>) { 558 * <jk>switch</jk> (<jp>property</jp>) { 559 * <jk>case</jk> <js>"id"</js>: <jk>return</jk> <jv>entity</jv>.getPrimaryKey(); 560 * <jk>case</jk> <js>"displayName"</js>: <jk>return</jk> <jv>entity</jv>.computeDisplayName(); 561 * <jk>case</jk> <js>"metadata"</js>: <jk>return</jk> <jv>entity</jv>.getMetadataAsMap(); 562 * } 563 * } 564 * <jk>return</jk> <jk>null</jk>; <jc>// Let next extractor try</jc> 565 * }; 566 * 567 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 568 * .addPropertyExtractor(<jv>customExtractor</jv>) 569 * .defaultSettings() <jc>// Adds standard extractors</jc> 570 * .build(); 571 * </p> 572 * 573 * <p><b>Execution Order:</b> Custom extractors are tried before the default extractors 574 * added by {@link #defaultSettings()}, allowing overrides of standard behavior.</p> 575 * 576 * @param e The property extractor to register 577 * @return This builder for method chaining 578 * @see PropertyExtractor 579 * @see PropertyExtractors 580 */ 581 public Builder addPropertyExtractor(PropertyExtractor e) { propertyExtractors.add(e); return this; } 582 583 /** 584 * Adds default handlers and settings for common Java types. 585 * 586 * <p>This method registers comprehensive support for:</p> 587 * <ul> 588 * <li><b>Collections:</b> List, Set, Collection → bracket format</li> 589 * <li><b>Maps:</b> Map, Properties → brace format with key=value pairs</li> 590 * <li><b>Map Entries:</b> Map.Entry → <js>"key=value"</js> format</li> 591 * <li><b>Dates:</b> Date, Calendar → ISO-8601 format</li> 592 * <li><b>Files/Streams:</b> File, InputStream, Reader → content extraction</li> 593 * <li><b>Reflection:</b> Class, Method, Constructor → readable signatures</li> 594 * <li><b>Enums:</b> All enum types → name() format</li> 595 * <li><b>Iterables:</b> Iterable, Iterator, Enumeration, Stream → list conversion</li> 596 * <li><b>Wrappers:</b> Optional, Supplier → unwrapping/evaluation</li> 597 * </ul> 598 * 599 * <p>Default settings include:</p> 600 * <ul> 601 * <li><code>nullValue</code> = <js>"<null>"</js></li> 602 * <li><code>emptyValue</code> = <js>"<empty>"</js></li> 603 * <li><code>classNameFormat</code> = <js>"simple"</js></li> 604 * </ul> 605 * 606 * <p><b>Note:</b> This should typically be called after custom handlers to avoid 607 * overriding your custom configurations, since handlers are processed in reverse order.</p> 608 * 609 * @return This builder for method chaining 610 */ 611 public Builder defaultSettings() { 612 addSetting(SETTING_nullValue, "<null>"); 613 addSetting(SETTING_selfValue, "<self>"); 614 addSetting(SETTING_classNameFormat, "simple"); 615 616 addStringifier(Map.Entry.class, Stringifiers.mapEntryStringifier()); 617 addStringifier(GregorianCalendar.class, Stringifiers.calendarStringifier()); 618 addStringifier(Date.class, Stringifiers.dateStringifier()); 619 addStringifier(InputStream.class, Stringifiers.inputStreamStringifier()); 620 addStringifier(byte[].class, Stringifiers.byteArrayStringifier()); 621 addStringifier(Reader.class, Stringifiers.readerStringifier()); 622 addStringifier(File.class, Stringifiers.fileStringifier()); 623 addStringifier(Enum.class, Stringifiers.enumStringifier()); 624 addStringifier(Class.class, Stringifiers.classStringifier()); 625 addStringifier(Constructor.class, Stringifiers.constructorStringifier()); 626 addStringifier(Method.class, Stringifiers.methodStringifier()); 627 addStringifier(List.class, Stringifiers.listStringifier()); 628 addStringifier(Map.class, Stringifiers.mapStringifier()); 629 630 // Note: Listifiers are processed in reverse registration order (last registered wins). 631 // Collection must be registered after Iterable so it takes precedence for Sets, 632 // ensuring TreeSet conversion for deterministic ordering. 633 addListifier(Iterable.class, Listifiers.iterableListifier()); 634 addListifier(Collection.class, Listifiers.collectionListifier()); 635 addListifier(Iterator.class, Listifiers.iteratorListifier()); 636 addListifier(Enumeration.class, Listifiers.enumerationListifier()); 637 addListifier(Stream.class, Listifiers.streamListifier()); 638 addListifier(Map.class, Listifiers.mapListifier()); 639 640 addSwapper(Optional.class, Swappers.optionalSwapper()); 641 addSwapper(Supplier.class, Swappers.supplierSwapper()); 642 addSwapper(Future.class, Swappers.futureSwapper()); 643 644 addPropertyExtractor(new PropertyExtractors.ObjectPropertyExtractor()); 645 addPropertyExtractor(new PropertyExtractors.ListPropertyExtractor()); 646 addPropertyExtractor(new PropertyExtractors.MapPropertyExtractor()); 647 648 return this; 649 } 650 651 /** 652 * Builds the configured BasicBeanConverter instance. 653 * 654 * <p>This method creates a new BasicBeanConverter with all registered handlers 655 * and settings. The builder can be reused to create multiple converters with 656 * the same configuration.</p> 657 * 658 * @return A new BasicBeanConverter instance 659 */ 660 public BasicBeanConverter build() { 661 return new BasicBeanConverter(this); 662 } 663 } 664 665 static class StringifierEntry<T> { 666 private Class<T> forClass; 667 private Stringifier<T> function; 668 669 private StringifierEntry(Class<T> forClass, Stringifier function) { 670 this.forClass = forClass; 671 this.function = function; 672 } 673 } 674 675 static class ListifierEntry<T> { 676 private Class<T> forClass; 677 private Listifier<T> function; 678 679 private ListifierEntry(Class<T> forClass, Listifier<T> function) { 680 this.forClass = forClass; 681 this.function = function; 682 } 683 } 684 685 static class SwapperEntry<T> { 686 private Class<T> forClass; 687 private Swapper<T> function; 688 689 private SwapperEntry(Class<T> forClass, Swapper<T> function) { 690 this.forClass = forClass; 691 this.function = function; 692 } 693 } 694}