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