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.time.format.DateTimeFormatter.*; 020import static java.util.stream.Collectors.*; 021import static org.apache.juneau.junit.bct.Utils.*; 022 023import java.io.*; 024import java.lang.reflect.*; 025import java.nio.file.*; 026import java.util.*; 027 028/** 029 * Collection of standard stringifier implementations for the Bean-Centric Testing framework. 030 * 031 * <p>This class provides built-in string conversion strategies that handle common Java types 032 * and objects. These stringifiers are automatically registered when using 033 * {@link BasicBeanConverter.Builder#defaultSettings()}.</p> 034 * 035 * <h5 class='section'>Purpose:</h5> 036 * <p>Stringifiers convert objects to human-readable string representations for use in BCT 037 * assertions and test output. They provide consistent, meaningful string formats across 038 * different object types while supporting customization for specific testing needs.</p> 039 * 040 * <h5 class='section'>Built-in Stringifiers:</h5> 041 * <ul> 042 * <li><b>{@link #mapEntryStringifier()}</b> - Converts {@link Map.Entry} to <js>"key=value"</js> format</li> 043 * <li><b>{@link #calendarStringifier()}</b> - Converts {@link GregorianCalendar} to ISO-8601 format</li> 044 * <li><b>{@link #dateStringifier()}</b> - Converts {@link Date} to ISO instant format</li> 045 * <li><b>{@link #inputStreamStringifier()}</b> - Converts {@link InputStream} content to hex strings</li> 046 * <li><b>{@link #byteArrayStringifier()}</b> - Converts byte arrays to hex strings</li> 047 * <li><b>{@link #readerStringifier()}</b> - Converts {@link Reader} content to strings</li> 048 * <li><b>{@link #fileStringifier()}</b> - Converts {@link File} content to strings</li> 049 * <li><b>{@link #enumStringifier()}</b> - Converts {@link Enum} values to name format</li> 050 * <li><b>{@link #classStringifier()}</b> - Converts {@link Class} objects to name format</li> 051 * <li><b>{@link #constructorStringifier()}</b> - Converts {@link Constructor} to signature format</li> 052 * <li><b>{@link #methodStringifier()}</b> - Converts {@link Method} to signature format</li> 053 * <li><b>{@link #listStringifier()}</b> - Converts {@link List} to bracket-delimited format</li> 054 * <li><b>{@link #mapStringifier()}</b> - Converts {@link Map} to brace-delimited format</li> 055 * </ul> 056 * 057 * <h5 class='section'>Usage Example:</h5> 058 * <p class='bjava'> 059 * <jc>// Register stringifiers using builder</jc> 060 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 061 * .defaultSettings() 062 * .addStringifier(Date.<jk>class</jk>, Stringifiers.<jsm>dateStringifier</jsm>()) 063 * .addStringifier(File.<jk>class</jk>, Stringifiers.<jsm>fileStringifier</jsm>()) 064 * .build(); 065 * </p> 066 * 067 * <h5 class='section'>Resource Handling:</h5> 068 * <p><b>Warning:</b> Some stringifiers consume or close their input resources:</p> 069 * <ul> 070 * <li><b>{@link InputStream}:</b> Stream is consumed and closed during stringification</li> 071 * <li><b>{@link Reader}:</b> Reader is consumed and closed during stringification</li> 072 * <li><b>{@link File}:</b> File content is read completely during stringification</li> 073 * </ul> 074 * 075 * <h5 class='section'>Custom Stringifier Development:</h5> 076 * <p>When creating custom stringifiers, follow these patterns:</p> 077 * <ul> 078 * <li><b>Null Safety:</b> Handle <jk>null</jk> inputs gracefully</li> 079 * <li><b>Resource Management:</b> Properly close resources after use</li> 080 * <li><b>Exception Handling:</b> Convert exceptions to meaningful error messages</li> 081 * <li><b>Performance:</b> Consider string building efficiency for complex objects</li> 082 * <li><b>Readability:</b> Ensure output is useful for debugging and assertions</li> 083 * </ul> 084 * 085 * @see Stringifier 086 * @see BasicBeanConverter.Builder#addStringifier(Class, Stringifier) 087 * @see BasicBeanConverter.Builder#defaultSettings() 088 */ 089@SuppressWarnings({"rawtypes"}) 090public class Stringifiers { 091 092 private static final char[] HEX = "0123456789ABCDEF".toCharArray(); 093 094 /** 095 * Constructor. 096 */ 097 private Stringifiers() {} 098 099 /** 100 * Returns a stringifier for {@link Map.Entry} objects that formats them as <js>"key=value"</js>. 101 * 102 * <p>This stringifier creates a human-readable representation of map entries by converting 103 * both the key and value to strings and joining them with the configured entry separator.</p> 104 * 105 * <h5 class='section'>Behavior:</h5> 106 * <ul> 107 * <li><b>Format:</b> Uses the pattern <js>"{key}{separator}{value}"</js></li> 108 * <li><b>Separator:</b> Uses the {@code mapEntrySeparator} setting (default: <js>"="</js>)</li> 109 * <li><b>Recursive conversion:</b> Both key and value are converted using the same converter</li> 110 * </ul> 111 * 112 * <h5 class='section'>Usage Examples:</h5> 113 * <p class='bjava'> 114 * <jc>// Test map entry stringification</jc> 115 * <jk>var</jk> <jv>entry</jv> = Map.<jsm>entry</jsm>(<js>"name"</js>, <js>"John"</js>); 116 * <jsm>assertBean</jsm>(<jv>entry</jv>, <js>"<self>"</js>, <js>"name=John"</js>); 117 * 118 * <jc>// Test with custom separator</jc> 119 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 120 * .defaultSettings() 121 * .addSetting(<jsf>SETTING_mapEntrySeparator</jsf>, <js>": "</js>) 122 * .build(); 123 * <jsm>assertBean</jsm>(<jv>entry</jv>, <js>"<self>"</js>, <js>"name: John"</js>); 124 * </p> 125 * 126 * @return A {@link Stringifier} for {@link Map.Entry} objects 127 * @see Map.Entry 128 */ 129 public static Stringifier<Map.Entry> mapEntryStringifier() { 130 return (bc, entry) -> bc.stringify(entry.getKey()) + bc.getSetting("mapEntrySeparator", "=") + bc.stringify(entry.getValue()); 131 } 132 133 /** 134 * Returns a stringifier for {@link GregorianCalendar} objects that formats them as ISO-8601 strings. 135 * 136 * <p>This stringifier converts calendar objects to standardized ISO-8601 timestamp format, 137 * which provides consistent, sortable, and internationally recognized date representations.</p> 138 * 139 * <h5 class='section'>Behavior:</h5> 140 * <ul> 141 * <li><b>Format:</b> Uses the {@code calendarFormat} setting (default: {@link java.time.format.DateTimeFormatter#ISO_INSTANT})</li> 142 * <li><b>Timezone:</b> Respects the calendar's timezone information</li> 143 * <li><b>Precision:</b> Includes full precision available in the calendar</li> 144 * </ul> 145 * 146 * <h5 class='section'>Usage Examples:</h5> 147 * <p class='bjava'> 148 * <jc>// Test calendar stringification</jc> 149 * <jk>var</jk> <jv>calendar</jv> = <jk>new</jk> GregorianCalendar(<jv>2023</jv>, Calendar.<jsf>JANUARY</jsf>, <jv>15</jv>); 150 * <jsm>assertMatchesGlob</jsm>(<js>"2023-01-*"</js>, <jv>calendar</jv>); 151 * 152 * <jc>// Test with custom format</jc> 153 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 154 * .defaultSettings() 155 * .addSetting(<jsf>SETTING_calendarFormat</jsf>, DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>) 156 * .build(); 157 * </p> 158 * 159 * @return A {@link Stringifier} for {@link GregorianCalendar} objects 160 * @see GregorianCalendar 161 * @see java.time.format.DateTimeFormatter#ISO_INSTANT 162 */ 163 public static Stringifier<GregorianCalendar> calendarStringifier() { 164 return (bc, calendar) -> calendar.toZonedDateTime().format(bc.getSetting("calendarFormat", ISO_INSTANT)); 165 } 166 167 /** 168 * Returns a stringifier for {@link Date} objects that formats them as ISO instant strings. 169 * 170 * <p>This stringifier converts Date objects to ISO-8601 instant format, providing 171 * standardized timestamp representations suitable for logging and comparison.</p> 172 * 173 * <h5 class='section'>Behavior:</h5> 174 * <ul> 175 * <li><b>Format:</b> ISO-8601 instant format (e.g., <js>"2023-01-15T10:30:00Z"</js>)</li> 176 * <li><b>Timezone:</b> Always represents time in UTC (Z timezone)</li> 177 * <li><b>Precision:</b> Millisecond precision as available in Date objects</li> 178 * </ul> 179 * 180 * <h5 class='section'>Usage Examples:</h5> 181 * <p class='bjava'> 182 * <jc>// Test date stringification</jc> 183 * <jk>var</jk> <jv>date</jv> = <jk>new</jk> Date(<jv>1673780400000L</jv>); <jc>// 2023-01-15T10:00:00Z</jc> 184 * <jsm>assertBean</jsm>(<jv>date</jv>, <js>"<self>"</js>, <js>"2023-01-15T10:00:00Z"</js>); 185 * 186 * <jc>// Test in object property</jc> 187 * <jk>var</jk> <jv>event</jv> = <jk>new</jk> Event().setTimestamp(<jv>date</jv>); 188 * <jsm>assertBean</jsm>(<jv>event</jv>, <js>"timestamp"</js>, <js>"2023-01-15T10:00:00Z"</js>); 189 * </p> 190 * 191 * @return A {@link Stringifier} for {@link Date} objects 192 * @see Date 193 */ 194 public static Stringifier<Date> dateStringifier() { 195 return (bc, date) -> date.toInstant().toString(); 196 } 197 198 /** 199 * Returns a stringifier for {@link InputStream} objects that converts content to hex strings. 200 * 201 * <p><b>Warning:</b> This stringifier consumes and closes the input stream during conversion. 202 * After stringification, the stream cannot be used again.</p> 203 * 204 * <h5 class='section'>Behavior:</h5> 205 * <ul> 206 * <li><b>Content reading:</b> Reads all available bytes from the stream</li> 207 * <li><b>Hex conversion:</b> Converts bytes to uppercase hexadecimal representation</li> 208 * <li><b>Resource management:</b> Automatically closes the stream after reading</li> 209 * </ul> 210 * 211 * <h5 class='section'>Usage Examples:</h5> 212 * <p class='bjava'> 213 * <jc>// Test with byte content</jc> 214 * <jk>var</jk> <jv>stream</jv> = <jk>new</jk> ByteArrayInputStream(<jk>new</jk> <jk>byte</jk>[]{<jv>0x48</jv>, <jv>0x65</jv>, <jv>0x6C</jv>, <jv>0x6C</jv>, <jv>0x6F</jv>}); 215 * <jsm>assertBean</jsm>(<jv>stream</jv>, <js>"<self>"</js>, <js>"48656C6C6F"</js>); <jc>// "Hello" in hex</jc> 216 * 217 * <jc>// Test empty stream</jc> 218 * <jk>var</jk> <jv>empty</jv> = <jk>new</jk> ByteArrayInputStream(<jk>new</jk> <jk>byte</jk>[<jv>0</jv>]); 219 * <jsm>assertBean</jsm>(<jv>empty</jv>, <js>"<self>"</js>, <js>""</js>); 220 * </p> 221 * 222 * <h5 class='section'>Important Notes:</h5> 223 * <ul> 224 * <li><b>One-time use:</b> The stream is consumed and closed during conversion</li> 225 * <li><b>Memory usage:</b> All content is loaded into memory for conversion</li> 226 * <li><b>Exception handling:</b> IO exceptions are wrapped in RuntimeException</li> 227 * </ul> 228 * 229 * @return A {@link Stringifier} for {@link InputStream} objects 230 * @see InputStream 231 */ 232 public static Stringifier<InputStream> inputStreamStringifier() { 233 return (bc, stream) -> stringifyInputStream(stream); 234 } 235 236 /** 237 * Returns a stringifier for byte arrays that converts them to hex strings. 238 * 239 * <p>This stringifier provides a consistent way to represent binary data as readable 240 * hexadecimal strings, useful for testing and debugging binary content.</p> 241 * 242 * <h5 class='section'>Behavior:</h5> 243 * <ul> 244 * <li><b>Hex format:</b> Each byte is represented as two uppercase hex digits</li> 245 * <li><b>No separators:</b> Bytes are concatenated without spaces or delimiters</li> 246 * <li><b>Empty arrays:</b> Returns empty string for zero-length arrays</li> 247 * </ul> 248 * 249 * <h5 class='section'>Usage Examples:</h5> 250 * <p class='bjava'> 251 * <jc>// Test byte array stringification</jc> 252 * <jk>byte</jk>[] <jv>data</jv> = {<jv>0x48</jv>, <jv>0x65</jv>, <jv>0x6C</jv>, <jv>0x6C</jv>, <jv>0x6F</jv>}; 253 * <jsm>assertBean</jsm>(<jv>data</jv>, <js>"<self>"</js>, <js>"48656C6C6F"</js>); <jc>// "Hello" in hex</jc> 254 * 255 * <jc>// Test with zeros and high values</jc> 256 * <jk>byte</jk>[] <jv>mixed</jv> = {<jv>0x00</jv>, <jv>0xFF</jv>, <jv>0x7F</jv>}; 257 * <jsm>assertBean</jsm>(<jv>mixed</jv>, <js>"<self>"</js>, <js>"00FF7F"</js>); 258 * </p> 259 * 260 * @return A {@link Stringifier} for byte arrays 261 */ 262 public static Stringifier<byte[]> byteArrayStringifier() { 263 return (bc, bytes) -> { 264 var sb = new StringBuilder(bytes.length * 2); 265 for (var element : bytes) { 266 var v = element & 0xFF; 267 sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]); 268 } 269 return sb.toString(); 270 }; 271 } 272 273 /** 274 * Returns a stringifier for {@link Reader} objects that converts content to strings. 275 * 276 * <p><b>Warning:</b> This stringifier consumes and closes the reader during conversion. 277 * After stringification, the reader cannot be used again.</p> 278 * 279 * <h5 class='section'>Behavior:</h5> 280 * <ul> 281 * <li><b>Content reading:</b> Reads all available characters from the reader</li> 282 * <li><b>String conversion:</b> Converts characters directly to string format</li> 283 * <li><b>Resource management:</b> Automatically closes the reader after reading</li> 284 * </ul> 285 * 286 * <h5 class='section'>Usage Examples:</h5> 287 * <p class='bjava'> 288 * <jc>// Test with string content</jc> 289 * <jk>var</jk> <jv>reader</jv> = <jk>new</jk> StringReader(<js>"Hello World"</js>); 290 * <jsm>assertBean</jsm>(<jv>reader</jv>, <js>"<self>"</js>, <js>"Hello World"</js>); 291 * 292 * <jc>// Test with file reader</jc> 293 * <jk>var</jk> <jv>fileReader</jv> = Files.<jsm>newBufferedReader</jsm>(path); 294 * <jsm>assertMatchesGlob</jsm>(<js>"*expected content*"</js>, <jv>fileReader</jv>); 295 * </p> 296 * 297 * <h5 class='section'>Important Notes:</h5> 298 * <ul> 299 * <li><b>One-time use:</b> The reader is consumed and closed during conversion</li> 300 * <li><b>Memory usage:</b> All content is loaded into memory for conversion</li> 301 * <li><b>Exception handling:</b> IO exceptions are wrapped in RuntimeException</li> 302 * </ul> 303 * 304 * @return A {@link Stringifier} for {@link Reader} objects 305 * @see Reader 306 */ 307 public static Stringifier<Reader> readerStringifier() { 308 return (bc, reader) -> stringifyReader(reader); 309 } 310 311 /** 312 * Returns a stringifier for {@link File} objects that converts file content to strings. 313 * 314 * <p>This stringifier reads the entire file content and returns it as a string, 315 * making it useful for testing file-based operations and content verification.</p> 316 * 317 * <h5 class='section'>Behavior:</h5> 318 * <ul> 319 * <li><b>Content reading:</b> Reads the entire file content into memory</li> 320 * <li><b>Encoding:</b> Uses the default platform encoding for text files</li> 321 * <li><b>Resource management:</b> Properly closes file resources after reading</li> 322 * </ul> 323 * 324 * <h5 class='section'>Usage Examples:</h5> 325 * <p class='bjava'> 326 * <jc>// Test file content</jc> 327 * <jk>var</jk> <jv>configFile</jv> = <jk>new</jk> File(<js>"config.properties"</js>); 328 * <jsm>assertMatchesGlob</jsm>(<js>"*database.url=*"</js>, <jv>configFile</jv>); 329 * 330 * <jc>// Test empty file</jc> 331 * <jk>var</jk> <jv>emptyFile</jv> = <jk>new</jk> File(<js>"empty.txt"</js>); 332 * <jsm>assertBean</jsm>(<jv>emptyFile</jv>, <js>"<self>"</js>, <js>""</js>); 333 * </p> 334 * 335 * <h5 class='section'>Important Notes:</h5> 336 * <ul> 337 * <li><b>Memory usage:</b> Large files will consume significant memory</li> 338 * <li><b>File existence:</b> Non-existent files will cause exceptions</li> 339 * <li><b>Binary files:</b> May produce unexpected results with binary content</li> 340 * <li><b>Exception handling:</b> IO exceptions are wrapped in RuntimeException</li> 341 * </ul> 342 * 343 * @return A {@link Stringifier} for {@link File} objects 344 * @see File 345 */ 346 public static Stringifier<File> fileStringifier() { 347 return (bc, file) -> safe(() -> stringifyReader(Files.newBufferedReader(file.toPath()))); 348 } 349 350 /** 351 * Returns a stringifier for {@link Enum} objects that converts them to name format. 352 * 353 * <p>This stringifier provides a consistent way to represent enum values as their 354 * declared constant names, which is typically the most useful format for testing.</p> 355 * 356 * <h5 class='section'>Behavior:</h5> 357 * <ul> 358 * <li><b>Name format:</b> Uses {@link Enum#name()} method for string representation</li> 359 * <li><b>Case preservation:</b> Maintains the exact case as declared in enum</li> 360 * <li><b>All enum types:</b> Works with any enum implementation</li> 361 * </ul> 362 * 363 * <h5 class='section'>Usage Examples:</h5> 364 * <p class='bjava'> 365 * <jc>// Test enum stringification</jc> 366 * <jsm>assertBean</jsm>(Color.<jsf>RED</jsf>, <js>"<self>"</js>, <js>"RED"</js>); 367 * <jsm>assertBean</jsm>(Status.<jsf>IN_PROGRESS</jsf>, <js>"<self>"</js>, <js>"IN_PROGRESS"</js>); 368 * 369 * <jc>// Test in object property</jc> 370 * <jk>var</jk> <jv>task</jv> = <jk>new</jk> Task().setStatus(Status.<jsf>COMPLETED</jsf>); 371 * <jsm>assertBean</jsm>(<jv>task</jv>, <js>"status"</js>, <js>"COMPLETED"</js>); 372 * </p> 373 * 374 * <h5 class='section'>Alternative Formats:</h5> 375 * <p>If you need different enum string representations (like {@link Enum#toString()} 376 * or custom formatting), register a custom stringifier for specific enum types.</p> 377 * 378 * @return A {@link Stringifier} for {@link Enum} objects 379 * @see Enum 380 * @see Enum#name() 381 */ 382 public static Stringifier<Enum> enumStringifier() { 383 return (bc, enumValue) -> enumValue.name(); 384 } 385 386 /** 387 * Returns a stringifier for {@link Class} objects that formats them according to configured settings. 388 * 389 * <p>This stringifier provides flexible class name formatting, supporting different 390 * levels of detail from simple names to fully qualified class names.</p> 391 * 392 * <h5 class='section'>Behavior:</h5> 393 * <ul> 394 * <li><b>Format options:</b> Controlled by {@code classNameFormat} setting</li> 395 * <li><b>Simple format:</b> Class simple name (default)</li> 396 * <li><b>Canonical format:</b> Fully qualified canonical name</li> 397 * <li><b>Full format:</b> Complete class name including package</li> 398 * </ul> 399 * 400 * <h5 class='section'>Usage Examples:</h5> 401 * <p class='bjava'> 402 * <jc>// Test with default simple format</jc> 403 * <jsm>assertBean</jsm>(String.<jk>class</jk>, <js>"<self>"</js>, <js>"String"</js>); 404 * <jsm>assertBean</jsm>(ArrayList.<jk>class</jk>, <js>"<self>"</js>, <js>"ArrayList"</js>); 405 * 406 * <jc>// Test with canonical format</jc> 407 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 408 * .defaultSettings() 409 * .addSetting(<jsf>SETTING_classNameFormat</jsf>, <js>"canonical"</js>) 410 * .build(); 411 * <jsm>assertBean</jsm>(String.<jk>class</jk>, <js>"<self>"</js>, <js>"java.lang.String"</js>); 412 * </p> 413 * 414 * @return A {@link Stringifier} for {@link Class} objects 415 * @see Class 416 */ 417 public static Stringifier<Class> classStringifier() { 418 return Stringifiers::stringifyClass; 419 } 420 421 /** 422 * Returns a stringifier for {@link Constructor} objects that formats them as readable signatures. 423 * 424 * <p>This stringifier creates human-readable constructor signatures including the 425 * declaring class name and parameter types, useful for reflection-based testing.</p> 426 * 427 * <h5 class='section'>Behavior:</h5> 428 * <ul> 429 * <li><b>Format:</b> <js>"{ClassName}({paramType1},{paramType2},...)"</js></li> 430 * <li><b>Class names:</b> Uses the configured class name format</li> 431 * <li><b>Parameter types:</b> Includes all parameter types in declaration order</li> 432 * </ul> 433 * 434 * <h5 class='section'>Usage Examples:</h5> 435 * <p class='bjava'> 436 * <jc>// Test constructor stringification</jc> 437 * <jk>var</jk> <jv>constructor</jv> = String.<jk>class</jk>.getConstructor(<jk>char</jk>[].<jk>class</jk>); 438 * <jsm>assertBean</jsm>(<jv>constructor</jv>, <js>"<self>"</js>, <js>"String(char[])"</js>); 439 * 440 * <jc>// Test no-arg constructor</jc> 441 * <jk>var</jk> <jv>defaultConstructor</jv> = ArrayList.<jk>class</jk>.getConstructor(); 442 * <jsm>assertBean</jsm>(<jv>defaultConstructor</jv>, <js>"<self>"</js>, <js>"ArrayList()"</js>); 443 * </p> 444 * 445 * @return A {@link Stringifier} for {@link Constructor} objects 446 * @see Constructor 447 */ 448 public static Stringifier<Constructor> constructorStringifier() { 449 return (bc, constructor) -> 450 new StringBuilder() 451 .append(stringifyClass(bc, ((Constructor<?>) constructor).getDeclaringClass())) 452 .append('(') 453 .append( 454 Arrays.stream((constructor) 455 .getParameterTypes()) 456 .map(x -> stringifyClass(bc, x)) 457 .collect(joining(",")) 458 ) 459 .append(')') 460 .toString(); 461 } 462 463 /** 464 * Returns a stringifier for {@link Method} objects that formats them as readable signatures. 465 * 466 * <p>This stringifier creates human-readable method signatures including the method 467 * name and parameter types, useful for reflection-based testing and debugging.</p> 468 * 469 * <h5 class='section'>Behavior:</h5> 470 * <ul> 471 * <li><b>Format:</b> <js>"{methodName}({paramType1},{paramType2},...)"</js></li> 472 * <li><b>Method name:</b> Uses the declared method name</li> 473 * <li><b>Parameter types:</b> Includes all parameter types in declaration order</li> 474 * </ul> 475 * 476 * <h5 class='section'>Usage Examples:</h5> 477 * <p class='bjava'> 478 * <jc>// Test method stringification</jc> 479 * <jk>var</jk> <jv>method</jv> = String.<jk>class</jk>.getMethod(<js>"substring"</js>, <jk>int</jk>.<jk>class</jk>, <jk>int</jk>.<jk>class</jk>); 480 * <jsm>assertBean</jsm>(<jv>method</jv>, <js>"<self>"</js>, <js>"substring(int,int)"</js>); 481 * 482 * <jc>// Test no-arg method</jc> 483 * <jk>var</jk> <jv>toString</jv> = Object.<jk>class</jk>.getMethod(<js>"toString"</js>); 484 * <jsm>assertBean</jsm>(<jv>toString</jv>, <js>"<self>"</js>, <js>"toString()"</js>); 485 * </p> 486 * 487 * @return A {@link Stringifier} for {@link Method} objects 488 * @see Method 489 */ 490 public static Stringifier<Method> methodStringifier() { 491 return (bc, method) -> 492 new StringBuilder() 493 .append(method.getName()) 494 .append('(') 495 .append( 496 Arrays.stream(method.getParameterTypes()) 497 .map(x -> stringifyClass(bc, x)) 498 .collect(joining(",")) 499 ) 500 .append(')') 501 .toString(); 502 } 503 504 /** 505 * Returns a stringifier for {@link List} objects that formats them with configurable delimiters. 506 * 507 * <p>This stringifier converts lists to bracket-delimited strings with customizable 508 * separators and prefixes/suffixes, providing consistent list representation across tests.</p> 509 * 510 * <h5 class='section'>Behavior:</h5> 511 * <ul> 512 * <li><b>Format:</b> <js>"{prefix}{element1}{separator}{element2}...{suffix}"</js></li> 513 * <li><b>Separator:</b> Uses {@code fieldSeparator} setting (default: <js>","</js>)</li> 514 * <li><b>Prefix:</b> Uses {@code collectionPrefix} setting (default: <js>"["</js>)</li> 515 * <li><b>Suffix:</b> Uses {@code collectionSuffix} setting (default: <js>"]"</js>)</li> 516 * <li><b>Recursive:</b> Elements are converted using the same converter</li> 517 * </ul> 518 * 519 * <h5 class='section'>Usage Examples:</h5> 520 * <p class='bjava'> 521 * <jc>// Test list stringification</jc> 522 * <jk>var</jk> <jv>list</jv> = List.<jsm>of</jsm>(<js>"apple"</js>, <js>"banana"</js>, <js>"cherry"</js>); 523 * <jsm>assertBean</jsm>(<jv>list</jv>, <js>"<self>"</js>, <js>"[apple,banana,cherry]"</js>); 524 * 525 * <jc>// Test with custom formatting</jc> 526 * <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>() 527 * .defaultSettings() 528 * .addSetting(<jsf>SETTING_fieldSeparator</jsf>, <js>"; "</js>) 529 * .addSetting(<jsf>SETTING_collectionPrefix</jsf>, <js>"("</js>) 530 * .addSetting(<jsf>SETTING_collectionSuffix</jsf>, <js>")"</js>) 531 * .build(); 532 * <jsm>assertBean</jsm>(<jv>list</jv>, <js>"<self>"</js>, <js>"(apple; banana; cherry)"</js>); 533 * </p> 534 * 535 * @return A {@link Stringifier} for {@link List} objects 536 * @see List 537 */ 538 public static Stringifier<List> listStringifier() { 539 return (bc, list) -> ((List<?>)list).stream() 540 .map(bc::stringify) 541 .collect(joining( 542 bc.getSetting("fieldSeparator", ","), 543 bc.getSetting("collectionPrefix", "["), 544 bc.getSetting("collectionSuffix", "]") 545 )); 546 } 547 548 /** 549 * Returns a stringifier for {@link Map} objects that formats them with configurable delimiters. 550 * 551 * <p>This stringifier converts maps to brace-delimited strings by first converting the 552 * map to a list of entries and then stringifying each entry, providing consistent 553 * map representation across tests.</p> 554 * 555 * <h5 class='section'>Behavior:</h5> 556 * <ul> 557 * <li><b>Format:</b> <js>"{prefix}{entry1}{separator}{entry2}...{suffix}"</js></li> 558 * <li><b>Separator:</b> Uses {@code fieldSeparator} setting (default: <js>","</js>)</li> 559 * <li><b>Prefix:</b> Uses {@code mapPrefix} setting (default: <js>"{"</js>)</li> 560 * <li><b>Suffix:</b> Uses {@code mapSuffix} setting (default: <js>"}"</js>)</li> 561 * <li><b>Entry format:</b> Each entry uses the map entry stringifier</li> 562 * </ul> 563 * 564 * <h5 class='section'>Usage Examples:</h5> 565 * <p class='bjava'> 566 * <jc>// Test map stringification</jc> 567 * <jk>var</jk> <jv>map</jv> = Map.<jsm>of</jsm>(<js>"name"</js>, <js>"John"</js>, <js>"age"</js>, <jv>25</jv>); 568 * <jsm>assertMatchesGlob</jsm>(<js>"{*name=John*age=25*}"</js>, <jv>map</jv>); 569 * 570 * <jc>// Test empty map</jc> 571 * <jk>var</jk> <jv>emptyMap</jv> = Map.<jsm>of</jsm>(); 572 * <jsm>assertBean</jsm>(<jv>emptyMap</jv>, <js>"<self>"</js>, <js>"{}"</js>); 573 * </p> 574 * 575 * <h5 class='section'>Order Considerations:</h5> 576 * <p>The order of entries in the string depends on the map implementation's iteration 577 * order. Use order-independent assertions (like {@code assertMatchesGlob}) for maps where 578 * order is not guaranteed.</p> 579 * 580 * @return A {@link Stringifier} for {@link Map} objects 581 * @see Map 582 * @see Map.Entry 583 */ 584 public static Stringifier<Map> mapStringifier() { 585 return (bc, map) -> ((Map<?,?>)map).entrySet().stream() 586 .map(bc::stringify) 587 .collect(joining( 588 bc.getSetting("fieldSeparator", ","), 589 bc.getSetting("mapPrefix", "{"), 590 bc.getSetting("mapSuffix", "}") 591 )); 592 } 593 594 //--------------------------------------------------------------------------------------------- 595 // Helper methods for internal stringification 596 //--------------------------------------------------------------------------------------------- 597 598 /** 599 * Converts an InputStream to a hexadecimal string representation. 600 * 601 * @param stream The InputStream to convert 602 * @return Hexadecimal string representation of the stream content 603 */ 604 private static String stringifyInputStream(InputStream stream) { 605 return safe(() -> { 606 try (var o2 = stream) { 607 var buff = new ByteArrayOutputStream(1024); 608 var nRead = 0; 609 var b = new byte[1024]; 610 while ((nRead = o2.read(b, 0, b.length)) != -1) 611 buff.write(b, 0, nRead); 612 buff.flush(); 613 byte[] bytes = buff.toByteArray(); 614 var sb = new StringBuilder(bytes.length * 2); 615 for (var element : bytes) { 616 var v = element & 0xFF; 617 sb.append(HEX[v >>> 4]).append(HEX[v & 0x0F]); 618 } 619 return sb.toString(); 620 } 621 }); 622 } 623 624 /** 625 * Converts a Reader to a string representation. 626 * 627 * @param reader The Reader to convert 628 * @return String content from the reader 629 */ 630 private static String stringifyReader(Reader reader) { 631 return safe(() -> { 632 try (var o2 = reader) { 633 var sb = new StringBuilder(); 634 var buf = new char[1024]; 635 var i = 0; 636 while ((i = o2.read(buf)) != -1) 637 sb.append(buf, 0, i); 638 return sb.toString(); 639 } 640 }); 641 } 642 643 /** 644 * Converts a Class to a string representation based on converter settings. 645 * 646 * @param bc The bean converter for accessing settings 647 * @param clazz The Class to convert 648 * @return String representation of the class 649 */ 650 private static String stringifyClass(BeanConverter bc, Class<?> clazz) { 651 return switch(bc.getSetting("classNameFormat", "default")) { 652 case "simple" -> clazz.getSimpleName(); 653 case "canonical" -> clazz.getCanonicalName(); 654 default -> clazz.getName(); 655 }; 656 } 657}