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