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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</js>, <js>"RED"</js>);
367    *    <jsm>assertBean</jsm>(Status.<jsf>IN_PROGRESS</jsf>, <js>"&lt;self&gt;"</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>"&lt;self&gt;"</js>, <js>"String"</js>);
404    *    <jsm>assertBean</jsm>(ArrayList.<jk>class</jk>, <js>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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>"&lt;self&gt;"</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}