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