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